Technical Manual

Version0.2.0
DateOctober 2026
Chapter 1

Introduction and concepts

1.1 · What RootSpeak is

RootSpeak — Advanced Messaging System for Linux is the tool with which the administrators of a Linux machine send text messages to users and know who has confirmed reading them. It answers a question that the system's own tools leave open: “who has not read it yet?”

wall and write write to the terminals of logged-in users without keeping track of anything; notify-send shows a notification that may disappear; the motd and login banners are fixed texts. None of them keeps a state per message and per user. RootSpeak puts the pieces together (logind, the terminals, desktop autostart, zenity, the journal) and adds exactly this state.

RootSpeak in one figureAdministrator · rootrspeakC command, via sudoTUI · helprspeak alone · rspeak --helpjournalRSPEAK_* eventsMessage store · /var/lib/rootspeaksent/ID/reference copy · rootusers/U/U's mailbox · owned by UUser · Urootspeak-user promptshell hookrootspeak-user agentdesktop autostartterminals /dev/pts/N · zenity dialogas Ureads, confirms
Figure 1.1 — The three domains of RootSpeak: the root command, the message store, the helper that runs as the user

Version 0.2.0 is written in C (the 0.1.0 prototype was in bash): two programs, no daemon, no database. The rspeak command always runs as root and writes to the message store; on the user's side the helper rootspeak-user reads the mailbox and records the confirmation, in two ways: in the shell prompt (rootspeak-user prompt) and in the graphical session (rootspeak-user agent).

i
Note. this manual describes how RootSpeak works. The contract that every version must honour (states, invariants, interruptions, message store format) is chapter 19, “Specification”; the user manual explains how to use RootSpeak, without technical jargon.

1.2 · Basic concepts

A few concepts are enough to read the rest of the manual. They are the same in the code, in the tests and in the manuals.

Message
a text (at most 65536 bytes) with an optional title and an optional expiry, identified by a sequential number, the ID. The sender is the administrator who ran rspeak.
Recipient
a human user of the machine to whom the message is addressed: by name, by group (@group), everyone (all) or the logged-in users (online).
Reference copy
the message in /var/lib/rootspeak/sent/ID/, readable only by root: what was sent, to whom, when, with which expiry. It is the authoritative source.
Mailbox
a user's /var/lib/rootspeak/users/U/ folder: a working copy that the user controls, with the subfolders inbox, read, acks and state.
Channel
where the user sees the message and confirms it: a terminal (terminal pts/N, terminal tty3) or the desktop (desktop).
Confirmation
the user's statement of having read the message, written into their mailbox in acks/ID with date and channel. The first one counts.
Postponement
the answer “no” in the terminal or “Later” on the desktop: the question comes back after RSPEAK_REMIND_MINUTES minutes, or at the next login.
State
for each recipient exactly one of three: sent, delivered, confirmed. Revoked and expired are properties of the message, not states.
Three states, two propertiessentno copy in the mailboxdeliveredcopy in users/U/inboxconfirmedstatement in acks/IDverified copy“I have read it” or “y”message properties: revoked · expireda message is summarised by counting its recipients in the three states
Figure 1.2 — The three states of a recipient; revocation and expiry are properties of the message. The user confirms with “I have read it” on the desktop or “y” in the terminal

1.3 · Subsystems at a glance

The code lives in src/ and is organised in small modules with clear-cut responsibilities. This is the map that the manual explores chapter by chapter.

The modulesrspeak (root)rspeak.ccommands, recoverytui.cthe TUIhelp.crspeak --helpstore.cmailboxes, terminalsrspeak.hparts used by the TUIrootspeak-user (user)rootspeak-user.cprompt · agent · confirmationno module of its own besides this oneCommon parts, linked into both programscommon.cfiles, memorytext.ccleaning, formatevent.cjournaluser.cas_usersessions.clogind
Figure 1.3 — The modules of src/ and the program each one goes into (Makefile)
rspeak.c
the administrators' command: relaunch through sudo, send, list, status, revoke, remind, log, purge, recovery of interrupted sends (chapter 4, chapter 8, chapter 9, chapter 10).
rootspeak-user.c
the helper that runs as the user: the question at the prompt, the desktop agent, the recording of the confirmation (chapter 11, chapter 12, chapter 13).
store.c
the operations on the mailboxes, always with the user's identity; writing to the terminals; waking up the agent (chapter 7, chapter 8).
user.c
as_user: a child process with a user's identity and a time limit (chapter 7).
sessions.c
human users, groups, sessions and terminals, from passwd and from logind (chapter 6).
text.c
text cleaning, message format, configuration (chapter 5, chapter 4).
event.c
the structured events in the journal and their text (chapter 17).
tui.c
the TUI for administrators (chapter 14).
help.c
the built-in help of rspeak --help (chapter 15).
common.c
environment, errors, memory, growable strings, safe reading and writing of files (chapter 5.5, chapter 18).

1.4 · How to read this manual

If the question is…Where
What must always be true, whatever happens?chapter 19 · Specification
How is the message store organised, and who can write to it?chapter 5 and chapter 16
What happens, step by step, during a send?chapter 8
Why does root never write directly into the mailboxes?chapter 7
How does the question reach the terminal, and the desktop?chapter 11 and chapter 12
What exactly does “confirmed” mean?chapter 13 and chapter 19.1
How is the TUI built, and why does it have no states of its own?chapter 14
Where can the history of a message be seen?chapter 17
Which errors can RootSpeak report, and what do they mean?chapter 18
How is a change tested, and with what results?chapter 3.5 and chapter 20
Which defects have already been found and fixed?chapter 21
Table 1.1 — Where to find the answers
Chapter 2

General architecture

2.1 · Architectural principles

RootSpeak is small on purpose: two programs, files in a single folder, the systemd libraries already present on the system. The design favours verifiability: every state can be reconstructed by looking at the files, and every step that can be interrupted leaves a mark that the next command knows how to repair.

PrinciplesNo daemondelivery at send timeAlways as rootsudo before anythingNever blockone can always postponeThree statesfor each recipientRoot does not trustmailboxes as the userData, not coderootspeak.conf, messagesNo written statededuced from the filesA single sourcefor states and texts
Figure 2.1 — The eight principles that guide the code
PrincipleWhat it means in the code
No daemonDelivery is done by rspeak send at the moment of sending; the rest is done by the shell hook and the desktop agent, which start with the user's sessions. Recovery of interrupted sends runs at the start of the following commands.
rspeak always as rootThe command relaunches itself with sudo before looking at its arguments, including --help and version (chapter 4.1).
Never blockEvery message asks for confirmation and insists with reminders, but the user can always postpone. Sessions without a terminal are not touched.
Three statesFor each recipient: sent, delivered, confirmed. Expiry and revocation are properties of the message; the state_e enumeration in src/rspeak.h has only three values.
Root does not trust the mailboxesEvery access by root to users/U happens in a child process with U's identity, with a time limit (chapter 7).
Data, not codeConfiguration, messages and confirmations are data files read with size limits; nothing that comes from outside is executed.
No written stateThe state of a recipient is not written anywhere: it is deduced from sent/ID/delivery and from the mailbox (chapter 10.1).
A single sourceThe TUI uses the same functions as rspeak list and rspeak status; the built-in help and the user manual's reference come from the same texts.
Table 2.1 — The principles and their consequences

2.2 · The three privilege domains

A message crosses three domains with different privileges. The rule is simple: everything that passes between root and a user crosses a controlled boundary, in both directions.

Who is trustedTrusted · rootrspeakroot:admin group · 750sent/ · rootspeak.confwritten only by rootjournal_UID set by the systemBoundaryas_userchild with U's identityclean_texttexts and confirmations cleanedsize and time limits200 bytes, 20 secondsUntrusted · user Uusers/U/U can do anything in the mailboxrootspeak-userruns as UU's terminalsU's /dev/pts/N
Figure 2.2 — The three privilege domains and the boundary between root and the users
DomainWhoWritesReads
administratorrspeak, as rootsent/, the mailboxes (only as the user), the users' terminals, the journalthe whole message store; the mailboxes only as the user
userrootspeak-user, as Utheir own mailbox, the journal (with their own _UID)their own mailbox; never sent/
Table 2.2 — Who writes and who reads
i
Note. the TUI is not a separate domain: it is rspeak itself, and its actions are the commands run in a child process (chapter 14.1).

2.3 · The two programs

The Makefile produces two executables. The common parts (common text event user sessions) go into both; rspeak adds rspeak.o, help.o, store.o and tui.o.

Two programssrc/*.c10 modules, 8 headersbuild/rspeakrspeak, tui, help, store + commonbuild/rootspeak-userrootspeak-user + common$PREFIX/bin/rspeakroot:sudo · 750$PREFIX/lib/rootspeak/rootspeak-useroutside the PATH · 755makeinstall.sh
Figure 2.3 — From the sources to the installed programs
ProgramWherePermissionsWho runs it
rspeak/usr/local/bin/rspeak750, root: administrators' groupan administrator; it relaunches itself with sudo
rootspeak-user/usr/local/lib/rootspeak/rootspeak-user755the shell hook (prompt) and desktop autostart (agent); never the user by hand
Table 2.3 — The two programs

Both depend only on glibc and libsystemd (sd-login for sessions, sd-journal for the log). At run time they also need sudo, zenity, systemd-run and date; notify-send if available.

2.4 · The path of a message

The complete path touches every subsystem. Here it is in brief; each step has its own chapter.

The path of a messagerspeak sendrootsent/IDmessage storeU's mailboxusers/UTerminals/dev/pts/NU's agentdesktopincomplete, msg, recipientscopy as U, checkline in deliverytext, at most 3 sSIGUSR1 (as U)the user replies reads inbox/ID“I have read it”: acks/IDinbox/ID → read/ID
Figure 2.4 — The path of a message, from sending to confirmation
StepWhoEffectWhere
1rspeak sendresolves the recipients, cleans text and title, takes an ID, writes sent/ID/ with the incomplete markerchapter 8.2
2rspeak send as Uwrites users/U/inbox/ID (first .tmp, then renames), checks that it is there, then records the delivery in sent/ID/deliverychapter 8.3
3rspeak sendwrites the text to each of U's terminalschapter 8.4
4rspeak send as Uwakes the desktop agent with SIGUSR1, or starts it with systemd-run --userchapter 8.5
5hook or agentshow the message and ask for confirmationchapter 11, chapter 12
6rootspeak-usercreates acks/ID (the first one counts) and moves the message to read/chapter 13
7rspeak status, TUIdeduce the state by comparing sent/ID with the mailboxeschapter 10
Table 2.4 — The steps of a message

2.5 · Sessions and channels

RootSpeak does not use utmp, which does not record many modern terminals, but logind and the terminal devices. The useful question is not “how is the user logged in” but “on which channels can I reach them right now”.

User sessionHow RootSpeak finds itChannel
text console (tty3)logind session of type tty with terminal tty3terminal
ssh with a terminalpseudo-terminal /dev/pts/N owned by the userterminal
desktop terminal, tmuxpseudo-terminal owned by the userterminal
X11 or Wayland desktoplogind session of type x11 or waylanddesktop agent
ssh without a terminal, scp, cronno terminalnone: they are not touched
user not logged in—the mailbox, read at the next login
Table 2.5 — Sessions and delivery channels
  • The mailbox acts as a queue: a message that reaches no channel when it is sent appears at the next login (question in the terminal) or at the agent's first round (dialog).
  • Writing the text to a terminal does not count as a confirmation: the question arrives at the next prompt.
i
Note. a terminal in which the user switched identity with su still belongs to whoever opened it: rspeak send does not write to it. The shell hook reaches the user anyway at the next prompt.
Chapter 3

Repository, build and installation

3.1 · File map

The repository contains the C code, the files to install on the system, the manuals with their sources, the tests and the consistency checks. The following table is the file map: the lines are counted every time the manual is generated, and tools/check-docs.py fails if a code file is missing or if one that does not exist is listed.

RootSpeak/
├── src/                  # the C code: two programs, common parts
├── etc/                  # shell hooks, autostart, configuration
├── install.sh            # installation, upgrade, removal
├── Makefile              # build, tests, analysis, sanitizer
├── docs/                 # the two generated manuals, decisions, reviews
│   └── sources/          # generator, style, one file per chapter
├── tests/                # engine, TUI, help, units, fuzzing, container, VM
│   └── */results/        # results recorded with commit and fingerprint
└── tools/                # manual consistency, fingerprint, screenshots
Figure 3.1 — The folders of the repository
FileLinesRole
src/rspeak.c1436The administrators' command: relaunch through sudo, recipients, send, list, status, revoke, remind, log, purge, message summaries, recovery.
src/rspeak.h62The parts of rspeak also used by the TUI: recipient states, summary, commands.
src/tui.c1676The TUI for administrators: list, detail and history, writing and sending, actions with confirmation.
src/help.c709The built-in help: tabbed guide, manual-style page, plain text.
src/help.h11Interface of help.c.
src/store.c443The operations on the mailboxes, always with the user's identity; writing to terminals; waking up the agent.
src/store.h45Interface of store.c.
src/rootspeak-user.c628The helper that runs as the user: prompt (shell hook), agent (desktop), recording of confirmations.
src/user.c198as_user: a child process with a user's identity and a time limit.
src/user.h30Interface of user.c.
src/sessions.c175Human users, groups, sessions and terminals, from passwd, group and logind.
src/sessions.h22Interface of sessions.c.
src/text.c244Text cleaning, message format, configuration.
src/text.h48Interface of text.c: msg_t, conf_t.
src/event.c85Structured events in the journal and their text.
src/event.h22Interface of event.c.
src/common.c429Utilities: environment, errors, memory, growable strings, safe reading and writing of files.
src/common.h104Interface of common.c, version (RSPEAK_VERSION) and limits.
Makefile82Build, internal tests, static analysis, sanitizer.
install.sh114Installation, upgrade, removal.
etc/profile.d/rootspeak.sh23The hook for the shells that read /etc/profile, in plain sh: it hands bash over to rootspeak.bash and asks the others (except zsh) at login.
etc/rootspeak.bash24The bash hook: the question at login and at every prompt.
etc/rootspeak.zsh25The zsh hook, sourced by the system zshrc.
etc/rootspeak.fish22The fish hook, installed in /etc/fish/conf.d.
etc/xdg/autostart/rootspeak-agent.desktop7Autostart of the desktop agent.
etc/rootspeak/rootspeak.conf10The default configuration.
tests/engine.sh411Engine tests in test mode: confirmations, delivery, states, expiry, configuration, purge, agent, second review, reminder.
tests/unit-tests.c117Tests of the internal parts: cleaning, messages, configuration, events.
tests/tui.py187Test of the TUI on a virtual terminal: list, detail, sending, editor, reminder, revocation, search, sizes.
tests/help-tui.py104Test of the built-in help on a virtual terminal.
tests/fuzz/fuzz.c67Fuzzing with libFuzzer of what reads untrusted data (four targets).
tests/fuzz/run.sh36Builds and runs the fuzzing in a container with clang.
tests/container/Containerfile13The test machine: Debian 13, or any distribution with systemd, with sshd, several users and shells.
tests/container/setup.sh114Prepares the test machine on any distribution (apt, dnf, zypper, pacman): packages, sshd, shells, Italian locale, test users.
tests/container/run.sh50Starts the test machine, builds and installs RootSpeak there, runs the scenarios, saves the results.
tests/distros/run.sh96Compatibility tests on the test server: the scenarios on every distribution of the list, several at a time, with a summary.
tests/distros/list25The distributions of the compatibility tests and their container images.
tests/container/scenarios.py1004The real-world scenarios, run as root in the test machine (also in the VM, together with the desktop ones).
tests/vm/run.sh108Tests in a VM with a desktop on the test server: code, clean VM, scenarios, screenshots, results; one desktop profile per run.
tests/vm/prepare.sh43Builds once the base disk of a desktop profile, from the cloud image of its distribution.
tests/vm/users.sh49Run in the VM while its base disk is prepared: users, ssh keys, Italian locale.
tests/vm/desktops/debian-gnome.yaml48Desktop profile (cloud-init): Debian 13 with GNOME on Wayland.
tests/vm/desktops/ubuntu-gnome.yaml45Desktop profile: Ubuntu 24.04 with GNOME on Wayland.
tests/vm/desktops/ubuntu-cinnamon.yaml58Desktop profile: Cinnamon on X11 with LightDM on Ubuntu 24.04, the base of Linux Mint 22.
tests/vm/desktops/fedora-kde.yaml34Desktop profile: Fedora 44 with KDE Plasma on Wayland.
tests/vm/desktops/rocky-gnome.yaml50Desktop profile: Rocky Linux 9 with GNOME, SELinux enforcing.
tests/vm/start.sh28Starts a VM from a clean copy of its base disk.
tests/vm/stop.sh12Shuts down the VM and deletes the working disk.
tests/vm/capture.py15Captures the VM screen from the QEMU monitor.
tests/vm/desktop.py78Reads and presses the buttons of the RootSpeak dialogs through the desktop's accessibility (AT-SPI).
tools/check-docs.py166Consistency checks between code, help, manuals and tests.
tools/fingerprint.sh17Code fingerprint (SHA-256 of src, Makefile, etc, install.sh) and commit, recorded with the test results.
tools/tui-screenshots.sh97Real TUI screenshots for the manuals, captured in the test machine.
tools/setup-dev.sh108Says what a freshly cloned copy is missing (packages, git identity); --install installs the packages.
tools/backup.sh34The whole repository, with its history, in a single git bundle.
docs/sources/build.py645Generator of the two manuals and functions for figures and tables.
docs/sources/style.css148The common style of the manuals of the seven projects, embedded unchanged in every manual; the rules that only RootSpeak needs are in build.py.
docs/sources/manual.js326The common script of the manuals: search in the sidebar, index, Copy button on the command boxes.
Table 3.1 — File map

The chapters of the manuals are in docs/sources/user/ and docs/sources/technical/, one file per chapter; docs/decisions-and-history.md and CLAUDE.md collect the decisions and the working agreements, docs/adversarial-review.md the two external reviews. The bash prototype (version 0.1.0) is in the repository history.

3.2 · Building: the Makefile

The build is a Makefile with no external tools besides gcc and pkg-config (for libsystemd). Everything ends up in build/ (or in a folder chosen with B=); the installation folder of the helper is compiled into the program with -DLIBDIR.

Buildingmakerspeak, rootspeak-usermake unitbuild/unit-testsbuild/PIE, RELRO, warnings = errorsmake sanitizerbuild-san/ · ASan, UBSanmake analyzebuild-analyze/ · -fanalyzerinstall.shinstalls from build/tests/engine.shRSPEAK_BUILD=build-sansystem/usr/localresultsengine.json
Figure 3.2 — The Makefile targets and where they lead
TargetWhat it does
makebuilds build/rspeak and build/rootspeak-user
make unitbuilds and runs build/unit-tests in test mode
make analyzerebuilds everything in build-analyze/ with -fanalyzer
make sanitizerrebuilds in build-san/ with AddressSanitizer and UBSan, plus unit-tests; the engine is then run on them by hand with RSPEAK_BUILD=build-san tests/engine.sh
make docsregenerates the two manuals (python3 docs/sources/build.py)
make docs-checkruns tools/check-docs.py (chapter 20.5)
make testmake, make unit, make docs-check, then tests/engine.sh
make cleanremoves build, build-analyze and build-san
Table 3.2 — The Makefile targets
Makefile (excerpt)
WARNINGS   = -Wall -Wextra -Wpedantic -Wformat=2 -Wformat-security -Wshadow -Wpointer-arith \
           -Wcast-qual -Wwrite-strings -Wstrict-prototypes -Wmissing-prototypes -Wvla \
           -Wnull-dereference -Wimplicit-fallthrough -Wundef -Werror
HARDENING   = -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection -fcf-protection -fPIE
LDFLAGS += -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack

Warnings are errors (-Werror): the code does not build if gcc finds anything to object to. The meaning of each hardening flag is in chapter 16.3.

3.3 · install.sh step by step

Buildmake, if neededProgramsrspeak 750 · rootspeak-userHooksetc//var/lib/rootspeakusers 755 · sent 700Agentsclosed and restarted
Figure 3.3 — The stages of install.sh
  1. It relaunches itself with sudo if it is not root, and picks the administrators' group: the first one that exists among sudo, wheel, admin (otherwise root). It warns if zenity, loginctl or systemd-run are missing.
  2. If the programs are missing from build/, or were built for other folders (the LIBDIR path does not appear inside build/rspeak), it builds them with make PREFIX=… (gcc, make, pkg-config and libsystemd-dev are needed).
  3. It installs $PREFIX/lib/rootspeak/rootspeak-user (755) and $PREFIX/bin/rspeak with owner root:group (the administrators' group) and permissions 750. PREFIX is /usr/local. It removes the language catalogs left by the first 0.2.0 builds: RootSpeak speaks English only.
  4. It installs the manuals in $PREFIX/share/doc/rootspeak/, the configuration (only if missing), the shell hooks and the autostart entry, replacing @LIBDIR@ and @VARDIR@.
  5. It adds to /etc/bash.bashrc the line marked # RootSpeak hook (on Debian, interactive non-login shells do not read /etc/profile.d); if the system zshrc exists, it adds the line that sources rootspeak.zsh; for fish it writes /etc/fish/conf.d/rootspeak.fish.
  6. It creates /var/lib/rootspeak and users/ (755) and sent/ (700).
  7. It closes the agents of the previous version: they restart, updated, at the first send or at login.
OptionEffect
noneinstalls or upgrades
--uninstallremoves programs, manuals, hooks and the marked lines of bash.bashrc and of the zshrc, stops the agents; keeps messages and configuration
--purgelike --uninstall, and also removes /var/lib/rootspeak and /etc/rootspeak
Table 3.3 — The modes of install.sh

3.4 · What it installs, and where

PathOwnerPermissionsContents
/usr/local/bin/rspeakroot:sudo (or wheel, admin)750the administrators' command
/usr/local/lib/rootspeak/rootspeak-userroot755the helper, outside the PATH
/usr/local/lib/rootspeak/rootspeak.bash, rootspeak.zshroot644the bash and zsh hooks
/usr/local/share/doc/rootspeak/root644the two HTML manuals
/etc/rootspeak/rootspeak.confroot644the configuration (never overwritten)
/etc/profile.d/rootspeak.shroot644the hook for the shells that read /etc/profile
/etc/fish/conf.d/rootspeak.fishroot644the fish hook
/etc/xdg/autostart/rootspeak-agent.desktoproot644the agent's autostart entry
/etc/bash.bashrc, zshrc——a line marked # RootSpeak hook
/var/lib/rootspeak/root755 (sent/ 700)the message store (chapter 5)
Table 3.4 — What install.sh installs, and where
i
Note. users have no command: rootspeak-user is not in the PATH and, when run by hand without prompt or agent, replies that it is an internal helper and exits with 1.

3.5 · Before committing a change

  1. make (all warnings are errors) and make unit.
  2. python3 docs/sources/build.py, then tools/check-docs.py: generated manuals up to date, reference aligned with the help, file map complete, internal links valid, tests run on the current code (chapter 20.5).
  3. tests/engine.sh always; tests/help-tui.py if the help has changed; tests/tui.py and tools/tui-screenshots.sh if the TUI has changed; make sanitizer with RSPEAK_BUILD=build-san tests/engine.sh, make analyze and tests/fuzz/run.sh if the code that reads data has changed; tests/container/run.sh if sending, delivery, confirmations or permissions have changed; tests/vm/run.sh if the desktop has changed.
  4. A test of the affected flow in test mode (chapter 4.4); for the desktop, sudo and other users, a live test after ./install.sh, done by whoever has the root password.
!
Warning. test results are committed together with the change: each suite writes its result with the commit and the code fingerprint, and chapter 20 shows them next to the current fingerprint.
Chapter 4

Startup and configuration

4.1 · Starting rspeak

The main of src/rspeak.c is short and its order is a rule: nothing runs without root, not even the help. Before looking at the arguments it reads the environment; then, if it is not root and not in test mode, it relaunches itself with sudo.

Starting rspeakrspeak_initenvironment, RSPEAK_TESTroot or RSPEAK_TEST?execvp sudosudo -- /proc/self/exeno arguments?recovery, TUIstdin and stdout ttyhelp, versionhelp, --help, versioncommandrecovery, then the commanddie“rspeak: unknown command”noyesyesrelaunches unchanged
Figure 4.1 — Starting rspeak: environment, root, then the command
src/rspeak.c (in brief)
int main(int argc, char **argv)
{
	const char *cmd = argc > 1 ? argv[1] : "";

	rspeak_init("rspeak");
	/* As a non-root administrator, rspeak re-runs itself with sudo, even for
	 * the help and the version. */
	if (geteuid() != 0 && !rspeak_test)
		rerun_with_sudo(argv);
	umask(022);

	/* rspeak alone in a terminal opens the TUI; in a pipe, or with a
	 * terminal too small, the help */
	if (!*cmd && isatty(0) && isatty(1)) {
		recover();
		if (rspeak_tui())
			return 0;
	}
	if (!*cmd || !strcmp(cmd, "help") || !strcmp(cmd, "-h") || !strcmp(cmd, "--help")) {
		rspeak_help();
		return 0;
	}
	if (!strcmp(cmd, "version") || !strcmp(cmd, "--version")) {
		printf("RootSpeak - Advanced Messaging System for Linux %s\n", RSPEAK_VERSION);
		return 0;
	}
	if (strcmp(cmd, "send") && strcmp(cmd, "list") && strcmp(cmd, "status") && strcmp(cmd, "revoke") &&
	    strcmp(cmd, "remind") && strcmp(cmd, "log") && strcmp(cmd, "purge"))
		die("unknown command: %s (rspeak --help for the guide)", cmd);
	…
}
  • rerun_with_sudo reads its own path from /proc/self/exe and calls execvp("sudo", ["sudo", "--", path, arguments…]): the arguments arrive unchanged, and -- prevents them from being taken as sudo options.
  • Under sudo the environment is what sudo preserves: RSPEAK_TEST is not kept. SUDO_USER tells who launched the command: it becomes the sender (from) and the author of the events.
  • Recovery of interrupted sends (chapter 8.6) runs before send, list, status, revoke, remind, log and the TUI; not before purge, which skips incomplete sends by itself.
  • An unknown command ends with die: rspeak: unknown command: X (rspeak --help for the guide), exit status 1.

4.2 · Starting rootspeak-user

rootspeak-user never relaunches itself: it runs with the identity of whoever launches it, that is the user. Its main has a different order, dictated by the desktop agent.

StepWhy
1. handler for SIGUSR1it is the first statement: by default SIGUSR1 terminates the process, and rspeak send might send it to an agent that has just started (chapter 12.2)
2. rspeak_init("rootspeak-user"), conf_readenvironment, configuration
3. user from getuid and getpwuidthe mailbox is users/<name>: the name never comes from an argument
4. prompt [--login]the question in the terminal (chapter 11)
5. agentthe desktop agent (chapter 12)
6. confirm ID CHANNELonly in test mode: recording the confirmation on its own, for tests/engine.sh
otherwise“rootspeak-user: internal RootSpeak helper, not meant to be run directly”, exit status 1
Table 4.1 — Starting rootspeak-user

4.3 · The environment and test mode

rspeak_init in src/common.c sets the working folders. The defaults are compiled into the program; the environment variables count only in test mode: a root program does not take folders from the environment of whoever launches it.

Global variableDefaultIn test mode
rspeak_var/var/lib/rootspeakRSPEAK_VAR
rspeak_libdirLIBDIR from the Makefile (/usr/local/lib/rootspeak)RSPEAK_LIB
rspeak_conf_path/etc/rootspeak/rootspeak.confRSPEAK_CONF
rspeak_testfalsetrue if RSPEAK_TEST is not empty
rspeak_name—"rspeak" or "rootspeak-user": the prefix of error messages
Table 4.2 — The RootSpeak environment
RSPEAK_TEST=1
rspeak does not relaunch itself with sudo, does not wake or start agents, does not change identity in child processes and writes only to the “terminal” RSPEAK_TEST_TTY. An ordinary user who sets it keeps their own permissions (scenario G4: “Permission denied” as soon as rspeak is executed), and sudo removes it from the environment.
RSPEAK_TEST_TTY=file
the “terminal” that rspeak send writes to in test mode; empty: none.
RSPEAK_BUILD=folder
for the tests: the folder of the programs to test (for example build-san).

4.4 · Testing without root

The code is tested from the repository folder as an ordinary user, on a test message store, after make. This is how tests/engine.sh, tests/tui.py and tests/help-tui.py run.

A test without root
nicfio@server:~/ROOTSPEAK$ make
nicfio@server:~/ROOTSPEAK$ export RSPEAK_TEST=1 RSPEAK_LIB=$PWD/build
nicfio@server:~/ROOTSPEAK$ export RSPEAK_VAR=/tmp/test-store RSPEAK_CONF=/dev/null RSPEAK_TEST_TTY=/tmp/tty
nicfio@server:~/ROOTSPEAK$ build/rspeak send --to nicfio "Test"
Message 1 sent to 1 user.
nicfio@server:~/ROOTSPEAK$ script -qec 'build/rootspeak-user prompt' /dev/null
!
Warning. without RSPEAK_TEST, build/rspeak goes through sudo; if the sudo credentials are still valid it starts as root without asking anything, on the real message store. In tests always set test mode.
i
Note. in test mode the recipients are real users of the machine (getpwnam), but their mailboxes are folders of the test message store written with the identity of whoever runs the test: tests/engine.sh uses root as its second recipient.

4.5 · The configuration: rootspeak.conf

The configuration is a data file, not a script: in the bash prototype it was read with source, by root and by the users, and could execute code. Now conf_read in src/text.c reads at most 65536 bytes of it and conf_from_text accepts only two keys, with positive integers.

etc/rootspeak/rootspeak.conf
# RootSpeak - Advanced Messaging System for Linux: configuration
# A data file, not a script: KEY=value lines with positive whole numbers.
# Unknown keys and invalid values are ignored.

# After how many minutes a message postponed by the user is shown again
RSPEAK_REMIND_MINUTES=30

# Every how many seconds the desktop agent checks the messages again
# (new messages wake it at once anyway)
RSPEAK_AGENT_POLL=60
Data, not code/etc/rootspeak/rootspeak.confread with read_fileconf_from_textline by lineknown key?conf_tremind, pollignoredyesno# comments and spaces removed; at most 255 charactersvalue: digits only, at most 6, greater than zero
Figure 4.2 — How the configuration is read
KeyDefaultWho uses itWhen it is re-read
RSPEAK_REMIND_MINUTES30rootspeak-user prompt and the agent: how long a postponement lastsat every rootspeak-user prompt; at every round of the agent
RSPEAK_AGENT_POLL60the agent: the periodic check, in addition to the wake-upat every round of the agent
Table 4.3 — The configuration keys
  • Each line loses its comment (from # onwards) and all spaces; a line that is still longer than 255 characters is not valid.
  • The value must consist of digits only, at most 6, and be greater than zero: 0, -3, abc and $(…) are ignored and the default stays (tests/engine.sh, group 5).
  • Unknown keys are ignored; a missing file, or one that is not a regular file, counts as empty.
  • rspeak itself does not read the configuration: the two keys concern only what rootspeak-user does.
Chapter 5

The message store

5.1 · Structure

The message store is made of files and folders under /var/lib/rootspeak. One part belongs to root, one part to each user: the boundary between the two is the heart of RootSpeak security. There is no database and no index: every piece of information is in a file that can be read with cat.

/var/lib/rootspeak/                   # root 755
├── sent/                       # root 700 · root only
│   ├── .seq  .seq.lock         # last ID assigned; lock for flock
│   ├── 12/
│   │   ├── msg                 # the message (reference copy)
│   │   ├── recipients          # one recipient per line
│   │   ├── delivery            # “user date terminals” for each delivered copy
│   │   ├── lock                # held by rspeak send while delivering (flock)
│   │   ├── incomplete          # marker: send in progress or to be completed
│   │   ├── revoked             # date of revocation, if revoked
│   │   ├── revoking            # marker: revoked copies still to be removed
│   │   └── failed              # marker: send interrupted before it was saved
│   └── .deleting-9/            # a message that rspeak purge is deleting
└── users/                      # root 755
    └── mario/                  # mario 700 · the mailbox: only Mario and root
        ├── inbox/12            # to be shown
        ├── read/11             # confirmed or expired
        ├── acks/11             # “date channel” of the confirmation
        └── state/12.term       # postponement: the file's modification time counts
Figure 5.1 — The message store, with message 12 still to be confirmed and message 11 confirmed
i
Note. the files incomplete, revoking and failed are internal recovery markers: they say that an operation must be completed or has been cancelled. They are not states of a message or of a recipient, and no command shows them as such (chapter 19.2).

5.2 · Format of a message

A message is a UTF-8 text file: a key: value header, an empty line, the text. The same file, byte for byte, is the reference copy in sent/ID/msg and the copy in the mailbox of each recipient.

sent/12/msg
format: 1
id: 12
from: admin
date: 1790714288
title: Maintenance
expires: 1790800688

On Saturday from 8 am to 12 noon the server will be off for disk maintenance.
FieldContents
formatformat version: 1. Readers ignore the keys they do not know; a different format must be read by an implementation that knows it
idsequential number, from next_id
fromthe administrator: SUDO_USER if it is an existing user, otherwise the user running rspeak
dateseconds since the Unix epoch; mandatory: a message without a valid date is rejected
titletitle, possibly empty, on a single line (newlines and tabs become spaces)
expiresseconds since the Unix epoch; empty: no expiry
Table 5.1 — The header of a message
Function (src/text.c)What it does
msg_formatwrites the header in the order shown in the figure, then the text
msg_from_textreads line by line up to the first empty line; also accepts key: without a value; what follows is the text
msg_readreads a regular file, without following links, at most RSPEAK_MAX_MSG bytes (65536 + 8192), then msg_from_text
msg_expiredtrue if expires is present and is less than or equal to the given time
msg_header“Title · 29/09 22:15”, or just the date: the line that precedes the text in the terminal, in the dialog and in rspeak status
Table 5.2 — Reading and writing a message

5.3 · Message numbers

Every message has a sequential number. next_id in src/rspeak.c takes it under an exclusive flock on sent/.seq.lock: two simultaneous rspeak send runs never get the same number.

Sequential numbersrspeak send Asent/.seq.lockexclusive flocksent/rspeak send Bflock: takenflock: waits.seq = 11 → mkdir 12.seq ← 12 (write_atomic)releases.seq = 12 → mkdir 13
Figure 5.2 — Two simultaneous sends get different numbers
  • It reads the last number from sent/.seq (0 if missing or not a number), increments it and creates the folder with mkdir(…, 0700).
  • If the folder already exists, it moves on to the next number: this happens if .seq went backwards, for example after a power failure that lost the last write. No message is ever overwritten.
  • It writes the new value with write_atomic and releases the lock by closing the descriptor.
  • A number taken and never used (an interruption right afterwards) remains a gap in the numbering: harmless.

5.4 · Owners and permissions

PathOwnerPermissionsWhy
/var/lib/rootspeakroot755traversable by everyone to reach their own mailbox
sent/root700messages and recipients visible only to administrators
sent/ID/root700created by next_id
sent/ID/*root600written by rspeak with O_NOFOLLOW
users/root755only root creates mailboxes: a user can neither create nor move them
users/U/U700created by root and handed over to U; only U (and root) can enter it
users/U/inbox, read, acks, stateU755created by U, in a child with U's identity; protected by the 700 folder
inbox/ID, acks/ID, state/ID.*U644written by U
Table 5.3 — Owners and permissions

A user can delete or modify the files in their own mailbox: they only spoil their own copy. The reference copy stays in sent/, and a message deleted from the mailbox simply shows up as not confirmed. A confirmation written by hand remains a statement by the user (chapter 19.1).

!
Warning. the effective permissions of the files created by U also depend on the process umask: the child of as_user sets it to 022, rootspeak-user uses the one of the user's session. The real protection is the users/U folder at 700.

5.5 · Reading and writing files safely

All file I/O goes through a few functions in src/common.c with fixed rules. These are the functions that make it safe to read what users write and that guarantee durability after an interruption.

FunctionRule
read_file(path, max, nofollow, out)opens with O_NONBLOCK (a pipe does not block the open) and, if requested, O_NOFOLLOW; rejects anything that is not a regular file and files larger than max
read_fd(fd, max, out)reads to the end, keeps at most max bytes and discards the rest
write_all(fd, p, n)retries after partial writes and signal interruptions
write_atomic(path, p, n, mode)writes path.tmp (O_EXCL|O_NOFOLLOW), fsync, renames; on error removes the temporary file
sync_path, syncfs_pathfsync of a file or a folder; syncfs of the file system that contains it
exists, is_folderwith lstat: a symbolic link is not the folder it points to
list_numbers(dir, out)only names made of digits (at most 18), in numeric order
read_number(s, out)digits only, at most 18: no sign, no spaces, no overflow
Table 5.4 — The I/O rules
Atomic writingpath.tmpO_EXCL, O_NOFOLLOWwrite_allpartial ones toofsyncto diskrenameatomicpathcompleteerror: unlink(path.tmp)
Figure 5.3 — write_atomic: a reader sees the old file or the new one, never a half-written one

Every descriptor is opened with O_CLOEXEC: no open file is passed on to the programs launched by RootSpeak (zenity, sudo, systemd-run, the editor). Memory is requested with xmalloc and texts grow in a buf_t (chapter 23.5): no fixed-length buffer receives data that comes from outside.

5.6 · Preserving the message store

RootSpeak has no backup or restore command: the message store is made of files, and the only tools provided are the purge of old messages (rspeak purge, chapter 9.3) and complete removal (install.sh --purge).

WhatWhereNote
messages, recipients, deliveries, revocations/var/lib/rootspeak/sent/authoritative; root only
copies, confirmations, postponements/var/lib/rootspeak/users/per user; the confirmations are the state
event historysystem journalkept according to the journal's rules, not RootSpeak's
configuration/etc/rootspeak/rootspeak.confnot overwritten by upgrades
Table 5.5 — Where the data lives
  • A reset message store reuses message numbers, the journal does not: this is why rspeak log ID reads only the events after the message's sending date (chapter 17.4).
  • A copy taken while a send is in progress contains the incomplete marker: given how recovery works (chapter 8.6), after a restore that send would be completed by the first rspeak command.
!
Warning. backing up and restoring the message store have not been tested: whoever does it must preserve owners and permissions (in particular U's users/U with 700 and root's sent/ with 700).
Chapter 6

Recipients and sessions

6.1 · From --to to the list

resolve_recipients in src/rspeak.c turns the value of --to into the list of recipients. It does so before taking a number and before writing anything: an error here leaves no trace in the message store.

Resolving the recipients--to DESTmario,@developers,onlinestrtok_rsplits on commasallhuman_usersonlineonline_users@groupgroup_membersnamegetpwnam: must existlistno duplicates, sortedunknown group or user: die, no message created
Figure 6.1 — From --to to the list of recipients (resolve_recipients)
  • The value is split on commas; each part is one of the four forms in the figure. Empty parts (two commas in a row) are skipped; a space, on the other hand, becomes part of the name, which therefore does not exist: this is why the help asks for names separated by commas without spaces.
  • An unknown user or group ends with die (“no such user: nobodyhere”, “no such group: x”): the unknown user is scenario B6 on the test machine.
  • Duplicates are removed along the way (scenario B2) and the final list is in alphabetical order: this is the order of the deliveries and of sent/ID/recipients.
  • An empty list (for example a group with no members) ends with “no recipients” (scenario B7).
  • An explicit name may also be a system account or root: the filter on human users applies only to all and online.

6.2 · Human users, logged-in users, groups

Function (src/sessions.c)What it returnsSource
human_usersthe users with a UID between UID_MIN and UID_MAX and a shell that does not end in nologin or false/etc/login.defs (defaults 1000 and 60000), getpwent
online_usersthe human users with at least one logind session of class usersd_get_sessions, sd_session_get_class, sd_session_get_uid
group_members Gthe members listed in the group plus the users whose primary group is G; false if the group does not existgetgrnam, getpwent
user_terminals UU's pseudo-terminals and the consoles of U's text sessionssee chapter 6.3
graphical_session Utrue if U has a session of type x11 or waylandsd_session_get_type
Table 6.1 — Recipients and sessions

Everything goes through the system interfaces (getpwent, getgrnam, libsystemd's sd-login library): LDAP users or users from other name services work if the system exposes them this way. all excludes root and system accounts because they have a UID below UID_MIN or a non-login shell (scenario B4).

i
Note. online counts the sessions of class user and then filters them against the human users: the test VM showed that root and logged-in system accounts (for example the graphical login manager) ended up among the recipients (scenario B5). They are now excluded even when logged in.

6.3 · A user's terminals

user_terminals collects the terminals to which rspeak send writes the text when sending. There are two sources, because neither is enough on its own.

Where the terminals arePseudo-terminals · /dev/pts/dev/pts/Nowner = U?ssh, emulatorstmux, desktop terminalslstat: character device owned by Unames made only of digitsConsoles · logindU's sessiontype tty/dev/ttyNsd_session_get_ttyonly names «tty» + digitsuser_terminals(U)no duplicates, sorted
Figure 6.2 — The two sources of a user's terminals
SourceWhat it findsWhat it does not find
/dev/pts/N owned by Ussh with a terminal, desktop terminal emulators, tmux and screentext consoles; terminals opened with su (they stay owned by whoever opened them)
U's logind sessions of type ttytext consoles (tty1…tty6)pseudo-terminals that are not a session (tmux, emulators)
Table 6.2 — The sources of terminals
  • Here the owner of /dev/pts/N serves only to find the candidates: at the moment of writing, write_terminal reopens the terminal and checks the owner of the opened terminal (chapter 8.4). The name /dev/pts/N does not identify a session: between the search and the write it might pass to another user.
  • In test mode there is a single «terminal»: the file RSPEAK_TEST_TTY, if set.
  • An ssh session without a terminal (scp, remote commands) and services have no terminals: they are not touched (scenario C3).

6.4 · The graphical session

graphical_session tells whether a user has an X11 or Wayland session open. It is needed in two places, with the same code.

Who calls itWhy
rspeak send, through wake_agentonly users with an open desktop receive the wake-up or a new agent (chapter 8.5)
rootspeak-user agentthe agent stays alive as long as the user has a graphical session, then exits (chapter 12.1)
Table 6.3 — Who uses graphical_session

A user with several graphical sessions (for example a local desktop and a remote one that creates a new session) still has a single agent: the lock rootspeak-agent.lock lives in $XDG_RUNTIME_DIR, which is unique per user (chapter 12.1). The behaviour with several simultaneous desktops of the same user has not been tested.

Chapter 7

Root with the user's identity

7.1 · The danger of symbolic links

The mailbox users/U belongs to the user, who can replace its subfolders with symbolic links, pipes or odd files. If root wrote into it directly, it would follow the link and could write anywhere; if it read from it, it could show files the user is not allowed to read. This is why every access by root to the contents of a mailbox happens in a child process with the user's identity: as_user in src/user.c, in place of the runuser used by the prototype.

The danger of symbolic linksIf root wrote directlyrspeak (root)writes inbox/12users/mario/inboxreplaced by Mario…/etc…with a symlinkroot would writeinside /etcWith as_userrspeak (root)as_user(mario)child (mario)writes inbox/12/etcpermission deniedworst case: Mariospoils his own mailbox
Figure 7.1 — Why root accesses the mailboxes with the user's identity

In the child the user's permissions apply: a link to /etc leads to «permission denied», and the worst the user can achieve is to spoil their own copy. Scenario G1 on the test machine checks this: with the mailbox turned into a link to /etc, rspeak send writes nothing in /etc and the recipient stays “sent, not delivered”.

i
Note. only the folder users/U itself is touched by root: root creates it (700) inside users/, which belongs to root, and hands it over to U with lchown. If it already exists it must be a real folder (lstat); if it is still owned by root, because a send was interrupted between creation and handover, it is handed over at the next send (scenario F6).

7.2 · How the child process starts

as_user(u, fn, arg, in, out, max, seconds) runs fn(arg) in a child process with the identity of u. The parent passes it data on stdin, collects its output on a pipe and waits for it to finish, always with a time limit.

A child with the user's identityrspeak (root)as_userpipestdin · stdoutchildforkchild as Ufn(arg)forkstdin and stdout on pipesinitgroups, setresgid, setresuidsetuid(0) must fail data on stdinoutput, at most max byteswithin the time limit exit code (waitpid)timed out: SIGKILL
Figure 7.2 — How the child process starts and ends
src/user.c
static void child(const user_t *u, as_user_fn fn, void *arg, int in, int out)
{
	int nul = open("/dev/null", O_RDWR | O_CLOEXEC);

	if (nul < 0 || dup2(in >= 0 ? in : nul, 0) < 0 || dup2(out >= 0 ? out : nul, 1) < 0 || dup2(nul, 2) < 0)
		_exit(126);
	if (geteuid() == 0 && !rspeak_test) {
		/* the user's full identity: supplementary groups, then group, then user */
		if (initgroups(u->name, u->gid) < 0 || setresgid(u->gid, u->gid, u->gid) < 0 ||
		    setresuid(u->uid, u->uid, u->uid) < 0)
			_exit(126);
		if (getuid() != u->uid || geteuid() != u->uid || getegid() != u->gid || setuid(0) == 0)
			_exit(126);
	}
	if (chdir("/") < 0)
		_exit(126);
	umask(022);
	signal(SIGPIPE, SIG_DFL);
	{
		int r = fn(arg);

		fflush(stdout);
		_exit(r & 0xff);
	}
}
  • The order matters: first the supplementary groups (initgroups), then the group, then the user; after setresuid the group can no longer be changed.
  • The child checks that it can not become root again: if setuid(0) succeeded, the identity would not really have been assumed. Any anomaly ends with exit code 126, which the parent turns into an error.
  • After the identity change the kernel makes the process non-inspectable by the user (not «dumpable»): U cannot read its memory while it works.
  • In test mode, without root, the child keeps the identity of whoever runs the test.

7.3 · Time limits and results

The child runs with the user's identity, so the user can stop it (SIGSTOP) or slow it down. rspeak must not hang: the time limit applies both while the child is writing and afterwards, until it exits.

Result of as_userWhen
the exit code of fn (0–125, 127–255)the child exited normally
-1the child did not start (pipe2, fork); it could not assume the identity (code 126); it was killed by a signal; the time ran out (the parent kills it with SIGKILL)
Table 7.1 — The results of as_user
CallTime limitOutput collected
delivery, revocation, reminder, deletion20 snone: the exit code is what counts
reading a mailbox (read_mailbox)20 sat most 4 MiB
waking the agent5 snone
Table 7.2 — The time limits

The pipes are created with O_CLOEXEC; the child's stdin is written in non-blocking mode, alternating with reading the output via poll (at most one second per round), so a child that does not read does not block the parent. gcc's static analysis found pipes left open in an error path of as_user: fixed (chapter 20.7).

7.4 · Operations on the mailboxes

All operations on the mailboxes are functions in src/store.c that run inside the child. They are small on purpose: each does one thing, with paths built from the message number.

OperationCalled byIn the child (as U)Result
op_deliverdeliver_copy: sending and recoverycreates inbox read acks state if missing; if the message is already in read/ or acks/ it does nothing; otherwise it writes inbox/ID.tmp (O_EXCL|O_NOFOLLOW), renames it and checks that inbox/ID is a regular, non-empty file0 if the copy is there (or was already read)
op_takeremove_copy: revocationremoves inbox/ID, state/ID.term and state/ID.gui0 if the copy is no longer there
op_clear_postponementsclear_postponements: reminderremoves state/ID.term and state/ID.gui0 if none are left
op_removeremove_copies: deletionremoves inbox/ID, read/ID, acks/ID and the postponementsalways 0
op_readread_mailbox: list, status, TUIfor each ID received on stdin: first line of acks/ID (at most 200 bytes, only from a regular file) and most recent date of state/ID.*lines «A ID line» and «S ID date» (chapter 10.2)
op_wakewake_agentreads the ready PID, checks the command line, sends SIGUSR10 if woken (chapter 8.5)
Table 7.3 — The operations performed as the user
i
Note. revocation and deletion do not ask the user whether the folder exists: if users/U is not there (a user never reached) there is nothing to remove and the operation succeeds without starting the child.
Chapter 8

Sending and recovery

8.1 · Options and text

cmd_send in src/rspeak.c checks everything it can check before writing to the message store: options, text, expiry, recipients. An error at this stage ends with die and leaves nothing behind.

OptionValueCheck
--to DESTrecipients separated by commasmandatory (“missing --to”); resolved after the text (chapter 6.1)
--title TITLEone linecleaned like the text; newlines and tabs become spaces
--expires DURATION30m, 2h, 3d or a datecomputed at once; it must be in the future (“the expiry is already in the past”)
--file FILEthe text from a fileread up to 64 MiB; it counts only if there is no text on the command line
--—end of options: whatever follows is text even if it starts with -
Table 8.1 — The options of rspeak send
  • Options end at the first argument that is not an option: from there on the arguments, joined by a space, are the text. Without text and without --file the text is read from stdin (at most 64 MiB).
  • An unknown option (“unknown option: --level”) or one without a value (“missing value for --to”) ends at once. --level belonged to version 0.1.0 and is rejected (tests/engine.sh, group 3).
  • A duration is a number followed by m, h or d; everything else is interpreted by date -d … +%s in a child process (for example "2026-10-04 12:00"). Dates in words are those understood by date, in English.
  • Trailing newlines are removed from the text, which is then cleaned (chapter 8.7). A text made only of spaces is an “empty message”; beyond 65536 bytes, after cleaning, it is a “message too long”: the desktop dialog receives the text as an argument to zenity, and Linux does not accept an argument larger than 128 KiB.

8.2 · The sending sequence

After the checks, sending is a fixed sequence. The order is chosen so that every interruption leaves a state that the next command can recognise and repair.

A send, step by stepprepare_storevar, users, sentnext_idflock on .seq.locklock + markerlock, incompletemsg, recipientswrite_atomic, syncfor each recipientin alphabetical orderrevoked?deliver_copyas U, verifiedterminalstext, at most 3 s eachwake_agentSIGUSR1 or systemd-rundeliveryuser date nSENT eventlist of recipients, authormissing copies?die, exit 1incomplete stayssyncfs, syncincomplete removednoyes: stopsyesno
Figure 8.1 — The sequence of rspeak send
StepWhatWhy here
1prepare_store: creates /var/lib/rootspeak, users/ (755) and sent/ (700) if missingRootSpeak also works without install.sh (test mode)
2next_id: the number and the folder sent/ID/under flock: unique numbers (chapter 5.3)
3sent/ID/lock taken exclusively, then the marker incompletefrom here on recovery, revocation and deletion know that a send is in progress
4msg and recipients with write_atomic; fsync of incomplete, of sent/ID/ and of sent/message, recipients and marker on disk before any delivery
5for each recipient: if revoked exists, stop; otherwise copy, terminals, wake-up, line in delivery, event DELIVEREDa revocation stops the send at the next recipient
6event SENT with the list of recipients and the authorafter the deliveries: in the log the send appears last
7amissing copies: “Message N delivered to X of Y recipients.”, then die with the namesincomplete stays; the lock is released on exit; the next command retries
7beverything delivered: syncfs of users/, fsync of sent/ID/, incomplete removed, fsync againwhen the marker disappears, copies and deliveries are on disk
Table 8.2 — The steps of sending
A successful send
admin@server:~$ sudo rspeak send --to @developers --title Maintenance --expires 1d \
    "On Saturday from 8 am to 12 noon the server will be off for disk maintenance."
Message 12 sent to 2 users.

8.3 · Verified delivery

“Delivered” means that the copy was in the mailbox, verified, at the time of delivery. deliver_copy in src/store.c writes and checks in the same child process; only if it succeeds does rspeak send record the line in delivery.

Verified deliveryprepare_mailboxroot: users/U 700, lchownas_user20 salready read?result 0nothing to writeinbox/ID.tmpO_EXCL, O_NOFOLLOWrename, statregular, not emptydeliveryonly if result 0yesno
Figure 8.2 — deliver_copy: the copy is written as the user and verified
  • Root prepares only users/U (chapter 7.1); the subfolders and the copy are written by the child with U's identity.
  • If the message is already in read/ or acks/ the copy is not rewritten: recovery does not present again to a user a message already read or confirmed (tests/engine.sh, group 2).
  • The copy is born as inbox/ID.tmp and becomes inbox/ID through a rename: rootspeak-user, which lists only names made of digits, never sees a half-written file.
  • A failure for one recipient (mailbox not writable, link to /etc, timeout) does not stop the others: the event NOT_DELIVERED goes to the log and sending continues (scenario F1).

8.4 · Writing to the terminals

For each terminal from user_terminals (chapter 6.3), rspeak send writes the formatted text, in English whatever the language of the session. Writing to the terminal does not count as confirmation: the question comes at the next prompt.

The text written to a user's terminal: header with title and date, then the text
── RootSpeak · Message from the administrator · Maintenance · 30/09 16:55 ──
On Saturday from 8 am to 12 noon the server will be off for disk maintenance.
────────────────────────────────────────
StepWhatWhy
terminal_textheader with title and date, text, closing line; every \n becomes \r\nthe terminal may be in raw mode, for example inside an editor
write_terminalopens with O_WRONLY|O_NOCTTY|O_NONBLOCK; fstat of the opened terminal: a character device whose owner is the recipientbetween the search and the open, /dev/pts/N may pass to another user
writingin pieces, with a poll of 100 ms between attempts, for at most 3 secondsa blocked terminal (Ctrl+S, slow network) does not stop the send
countthe terminals written in full end up in the delivery line“written to 2 terminals” in rspeak status
Table 8.3 — Writing to the terminals
i
Note. in test mode the only «terminal» is the file RSPEAK_TEST_TTY, and the check on the file type is skipped; the one on the owner remains: tests/engine.sh (group 9) checks that /dev/null, owned by root, is not written to.

8.5 · Waking or starting the agent

After the copy, wake_agent makes the dialog appear on the desktop at once, without waiting for the agent's periodic check. If the user has no graphical session (or in test mode) it does nothing.

Wake-up or startrspeak sendrootchild as U5 sU's agentppollsystemd --userof Ugraphical_session(U)?as_user(op_wake)PID in rootspeak-agent.readycmdline = …/rootspeak-user agent?kill(PID, SIGUSR1)wait is interrupted no ready agent systemd-run --user --machine=U@.hoststarts rootspeak-user agent
Figure 8.3 — Waking the desktop agent, or starting it
  • The signal goes only to a ready agent: the PID written in /run/user/UID/rootspeak-agent.ready (at most 64 bytes, without following links), which the agent writes after it has prepared to receive the signal (chapter 12.2).
  • The signal is sent from a child with U's identity, and only if /proc/PID/cmdline is exactly LIBDIR/rootspeak-user followed by agent: it cannot reach other users' processes, nor a process of U that had inherited an old PID.
  • If there is no ready agent, systemd-run --user --machine=U@.host --collect --quiet starts one in the systemd user manager, which already knows the graphical environment of the session. If an agent was starting, the new one finds the lock and exits; the one that was starting reads the mailbox anyway on its first round (scenario I8: dialog in about half a second).
i
Note. the agent writes rootspeak-agent.ready in $XDG_RUNTIME_DIR, while rspeak send looks for it in /run/user/UID: they coincide in sessions started by logind, which is the supported case.

8.6 · Recovery

recover runs at the start of almost every command (chapter 4.1) and repairs what an interrupted command left half done: sends, revocations, deletions. It is idempotent: repeating it changes nothing.

Recoveryrspeak sendrootsent/IDMailbox U1Mailbox U2rspeak (later)any commandlock, incomplete, msgcopy, verifydelivery: U1interrupted: U2 has no copy incomplete? lock free?rechecks the copywrites the copydelivery: U2; removes incomplete
Figure 8.4 — An interrupted send is completed by the next command
FindsConditionDoes
sent/ID/incompletelock free, but msg or recipients missing or unreadablewrites failed with the date, removes incomplete, event CANCELLED, warning “message N was interrupted before being saved and has been cancelled”
sent/ID/incompletelock free, message revoked or expiredremoves the marker without delivering
sent/ID/incompletelock free, valid messagefor each recipient deliver_copy, including those already recorded; for the new ones: line in delivery with 0 terminals, event DELIVERED, wake-up; if none is missing: syncfs, fsync, removes incomplete; event RECOVERED with the number of new ones
sent/ID/revokinglock freeremoves the remaining copies (chapter 9.1)
sent/.deleting-ID/alwayscompletes the deletion (chapter 9.3)
any markerlock heldnothing: it is a send in progress, not an interrupted send
Table 8.4 — What recovery repairs
  • The lock tells a send in progress from an interrupted one: rspeak send holds it for the whole delivery and the kernel releases it when the process dies, even with SIGKILL. Without this rule recovery, started during a long send, delivered in parallel and recorded duplicates (found by the real-world suite; scenarios F3 and F5).
  • Copies already recorded are checked again because, after a power failure, a delivery line might be on disk while the copy is not (tests/engine.sh, group 9).
  • In the last recorded run on the test machine (scenario F3), an rspeak send killed with SIGKILL after 4 deliveries out of 68 was completed by the next command in less than a second.
!
Warning. recovering a send in which some copy still cannot be written leaves incomplete and warns: “message N still not delivered to K recipients (see rspeak status N)”. Every following command retries.

8.7 · Cleaning the text

A message's text and title come from an administrator but end up in the users' terminals: they must not be able to move the cursor, change the window title or write to the clipboard. clean_text in src/text.c removes, in three passes, everything a terminal could interpret as a command.

PassRemovedExampleWhy
1, per lineCSI sequences: ESC [, digits, ; or ?, a final characterESC [ 31 mcolours, cursor movements, erasures
1, per lineOSC sequences: ESC ] … BELESC ] 0 ; title BELwindow title, hyperlinks, clipboard
2C0 control characters, except tab and newline; DEL0x01, \r, 0x7Fbell, carriage return, leftover lone escapes
3C1 characters in UTF-8C2 80 … C2 9Fsome terminals interpret them as commands
Table 8.5 — What clean_text removes, in order
  • An OSC sequence without a final BEL is not removed as a sequence: its ESC disappears in pass 2 and the rest remains as harmless text (tests/unit-tests.c).
  • Normal UTF-8 text stays intact, including non-breaking spaces (C2 A0).
  • clean_text is applied to text and title before they are saved, to what rspeak reads from the mailboxes (chapter 10.2), to the lines of rspeak log and to what the TUI shows of the commands it ran.
  • It gives the same result as the bash prototype (sed and tr): the two were compared on 2000 random texts full of sequences and control characters before the prototype was removed. Scenario G2 checks that no ESC reaches the terminal.
Chapter 9

Revocation, reminders and deletion

9.1 · Revoking: rspeak revoke

rspeak revoke ID revokes a message: whoever has not seen it yet will not see it, open questions and dialogs close by themselves. The revocation takes place at the instant sent/ID/revoked appears: from then on no new delivery, and no confirmation with a later date counts.

rspeak revokerspeak revokerootsent/IDstorerspeak sendin progressMailbox Uusers/UQuestion, dialogof Urevoking markerrevoked ← date (on disk)REVOKED event flock(lock): waitsrevoked? yes: stopsreleases the lockas U: removes inbox/ID, postponementsremoves the revoking markerinbox/ID gone?they close within 1 s
Figure 9.1 — A revocation during a send in progress
StepWhatWhy
1marker sent/ID/revokingif the command is interrupted, recovery knows there are copies to remove
2sent/ID/revoked with the date, written with write_atomic; fsync of revoking and of the folder; event REVOKEDthe revocation is on disk before the mailboxes are touched; a second revocation of the same message rewrites neither the date nor the event
3flock on sent/ID/lock, blockingwaits for an rspeak send in progress, which checks revoked before each recipient and stops
4for each recipient, as the user: removes inbox/ID, state/ID.term, state/ID.guicopies already read or confirmed (read/, acks/) stay: they are the history
5all removed: revoking removed; “Message N revoked.”otherwise die: “message N revoked, but some copies could not be removed from the mailboxes: the next rspeak command will retry”
Table 9.1 — The steps of a revocation
  • Serialising revocation with sending comes from the second review: before, a copy could arrive after the revocation. tests/engine.sh (group 9) checks that rspeak revoke waits for a lock held for 2 seconds; scenario F7 revokes a send to many users while it is in progress, and no copy is left behind.
  • If the revocation arrives during the send, rspeak send ends with “Message N revoked while sending: delivery stopped.”: the recipients not yet reached stay sent.
  • The question in the terminal and the dialog check inbox/ID every second and close (scenarios E3 and I6); a confirmation that arrives between their last check and the revocation does not count, because its date is later than revoked (chapter 13.1).

9.2 · Reminding: rspeak remind

rspeak remind ID brings the question back at once to those who have not confirmed yet, even if they had postponed it. It rewrites nothing: it removes the postponements and wakes the agent.

The reminderrspeak remind IDor the m key in the TUIrevoked or expired?dienobody to remindrecipients()state of each onedeliveredremoves state/ID.* as U, wake-upsentnothing: has no copyconfirmednothingyesnoREMINDED event with the number of reminded recipients and the author
Figure 9.2 — rspeak remind: only those who have the copy and have not confirmed
  • A revoked or expired message cannot be reminded: “message N has been revoked: nobody to remind” (tests/engine.sh, group 10).
  • The recipients' state comes from recipients, the same function used by rspeak status: the reminder goes to those who are delivered. Those who are sent do not have the copy (recovery will complete it), those who have confirmed must not be disturbed. If everyone has already confirmed, the command says so (“Message N: everyone has already confirmed, nobody to remind.”) instead of “0 recipients reminded”, which looked like a fault in the first live test (tests/engine.sh, group 10).
  • For each of them, as the user, state/ID.term and state/ID.gui are removed; then wake_agent. The question comes back at the next prompt (scenario D5); the dialog reappears at once (scenario I11: 0.37 s in the last recorded run).
  • The reminder takes no lock: if a confirmation arrives in the meantime, removing the postponements of someone who has confirmed has no effect (chapter 19.4).
A reminder
admin@server:~$ sudo rspeak remind 12
Message 12: 3 recipients reminded.
i
Note. before the reminder existed, the test VM found an edge case: an rspeak remind arriving within a second of “Later” was lost, because the agent recorded the postponement on its next round. Now the agent records the answer as soon as zenity closes (chapter 12.3).

9.3 · Deleting: rspeak purge

rspeak purge DURATION deletes the messages sent more than DURATION ago, together with the copies, confirmations and postponements in the mailboxes. The events stay in the journal.

Duration90d, 12h, 30mOlder?date of sendingLock free?and not incompleteRenamesent/.deleting-IDMailboxesas each userFolderfiles and rmdir
Figure 9.3 — rspeak purge: the message disappears with a rename, then the mailboxes are cleaned
StepWhat
durationa number followed by m, h or d; otherwise “invalid duration: 7x (for example 90d, 12h, 30m)”
selectionfor each message: the sending date from msg (or, if the file cannot be read, the modification date of the folder); a message with incomplete is skipped
locknon-blocking flock on sent/ID/lock: a send in progress is skipped
renamesent/ID → sent/.deleting-ID, fsync of sent/, lock released, event DELETED with duration and author
mailboxesfor each recipient, as the user: removes inbox/ID, read/ID, acks/ID and the postponements
folderremoves the files in sent/.deleting-ID/ and the folder itself
Table 9.2 — The steps of a deletion
  • The rename is the point at which the message stops existing for all commands: list_numbers sees only names made of digits. rspeak list reads what it needs from each message before using it and skips a message that has disappeared in the meantime (summarise returns false).
  • A deletion interrupted after the rename is completed by the recovery of the next command (tests/engine.sh, group 9).
  • rspeak purge does not run recovery before itself: it skips incomplete sends anyway.
A purge
admin@server:~$ sudo rspeak purge 90d
Deleted 4 messages older than 90d.
!
Warning. after the deletion message numbers are not reused (the .seq sequence stays), but rspeak log of a deleted number no longer finds the message: its history can then be read only with journalctl RSPEAK_MESSAGE=ID.
Chapter 10

State: list, status and the summary

10.1 · The state deduced from the files

A recipient's state is not written anywhere: recipient_state in src/rspeak.c deduces it every time from sent/ID/delivery, from the confirmation in the mailbox and from the reference copy. The expiry and revocation that count are root's, not those of the copy, which the user might have altered.

How the state is decidedline in delivery?SENTconfirmation in acks/ID?DELIVEREDno confirmationdate ≤ expiry?DELIVEREDlate confirmationdate ≤ revocation?DELIVEREDconfirmed after revocationCONFIRMEDdate and channelyesnoyesnonoyesnoyes
Figure 10.1 — recipient_state: a recipient's state, deduced from the files
ConditionStateLine in rspeak status
no line for U in deliverysent“sent, not delivered”; if there is incomplete: “sent, not delivered: the copy could not be written (see the log; the next rspeak command will retry)”
line in delivery, no confirmationdelivered“delivered”, plus the details: “, written to N terminals”, “, postponed on …”; or “, not confirmed (revoked)” or “, not confirmed (expired)”
confirmation dated after the expiry in sent/ID/msgdelivered“delivered, not confirmed (expired; confirmation arrived after the expiry)”
confirmation dated after sent/ID/revokeddelivered“delivered, not confirmed (revoked)”
valid confirmationconfirmed“confirmed on 01/10 17:10 (terminal pts/2)”
Table 10.1 — The state deduced from the files
  • The date of the confirmation is the number before the space in acks/ID (0 if it is not a number); the rest of the line is the channel, stored as it is shown (“terminal pts/2”, “desktop”; channel_text).
  • A confirmation with the same date as the revocation counts; one from the second after does not (tests/engine.sh, group 9).
  • A recipient is in exactly one of the three states: the function returns a value of state_e (SENT, DELIVERED, CONFIRMED) plus, separately, if the confirmation was late, its date and channel.

10.2 · Reading the mailboxes

rspeak status and rspeak list read the confirmations from the mailboxes, which the users control. read_mailbox in src/store.c treats them as untrusted data and reads them once per user, in a single child process.

Reading the mailboxesrspeak (root)list, status, TUIchild as Uop_read, 20 sMailbox Uusers/UIDs from sent/, one per lineacks/ID: at most 200 bytesstat of state/ID.term and .gui“A ID line” · “S ID date”clean_text then mailbox_from_text
Figure 10.2 — A mailbox is read once, as the user
  • The child receives on stdin the numbers of the messages that exist in sent/ and looks only for those: thousands of fake files in acks/ do not matter (5000 files: rspeak list under a second, tests/engine.sh group 9; before the fix, 3.9 s).
  • Of acks/ID it reads at most 200 bytes, and only if it is a regular file opened without blocking: a pipe in place of the confirmation does not block rspeak status (group 9).
  • Of state/ID.* it reads the most recent modification date: that is the time of the postponement.
  • The child's output (at most 4 MiB) goes through clean_text before it is interpreted: a confirmation forged with escape sequences does not reach the administrator's terminal (scenario G3).
  • The mailboxes read stay in memory for the whole command; the TUI forgets them at every refresh (mailboxes_empty). Deliveries are read once per message (read_deliveries).

10.3 · A message's summary

In the list of messages, in rspeak list and in the TUI, a message is summarised by its recipients: how many are in each of the three states, in words and with a bar, plus the properties revoked and expired. No other word describes the state of a message (chapter 19.3).

src/rspeak.h
/* A message summarised with its recipients. */
typedef struct {
	long long id;
	msg_t m;
	int confirmed, delivered, sent;
	bool revoked, expired;
} summary_t;
FunctionWhat it does
summarise(id, r)reads message, recipients, deliveries and revocation date before counting; false if the message no longer exists, has no recipients or if the send was cancelled (failed)
summary_text(r, out)“14 confirmed · 5 delivered · 2 sent”, omitting zero counts; “all confirmed (58)” if nobody is missing
the TUI bar12 marks: █ confirmed (green), ▒ delivered (yellow), ░ sent (red); a single sent recipient still gets at least one mark
Table 10.2 — Summarising a message
i
Note. the phrase always uses the name of the state (“5 delivered”, “1 confirmed”): it is the same word as in the STATE column of rspeak status.

10.4 · rspeak list and rspeak status

The two query commands show the same state at two levels: the list of messages and the detail of one. Both run recovery first; after that, only reads, without locks.

rspeak list: one message per line (the messages in the screenshots of chapter 14)
admin@server:~$ sudo rspeak list
ID    DATE         TEXT                                      RECIPIENTS
1     01/10 10:49  Tonight at 11 pm the mail server is upda  all confirmed (3)
2     01/10 10:49  On Saturday from 8 am to 12 noon the ser  4 confirmed · 3 delivered · 1 sent
3     01/10 10:49  Your account expires on Friday: please d  1 delivered
4     01/10 10:49  [revoked] Reboot at 1 pm.                 2 delivered
  • Columns: ID, sending date (“01/10 10:49”), the first 40 columns of the first line of the text (preceded by [revoked] and [expired]), the summary phrase.
  • A cancelled send appears as “[send cancelled: interrupted before it was saved]”: it reports an internal marker, not a state.
rspeak status: one recipient per line, with the state lines of the first table of this chapter
admin@server:~$ sudo rspeak status 2
Message 2 · Maintenance · 01/10 10:49 · from root
  │ On Saturday from 8 am to 12 noon the server will be off for disk maintenance.
  │ Please save your work by Friday evening.

USER             STATE
admin            confirmed on 01/10 10:49 (terminal pts/0)
anna             confirmed on 01/10 10:49 (desktop)
dario            sent, not delivered: the copy could not be written (see the log; the next rspeak command will retry)
fabio            confirmed on 01/10 10:49 (desktop)
kora             delivered, postponed on 01/10 10:49
luca             delivered
mario            delivered, postponed on 01/10 10:49
zeno             confirmed on 01/10 10:49 (terminal pts/2)
  • Header: number, title and date, sender; then “REVOKED on …” and “EXPIRED” if they apply; then the text, indented.
  • One line per recipient, in the order of recipients (alphabetical), with the state line from the previous table; the same lines appear in the TUI's detail view.
  • A non-existent or cancelled message ends with an error: “no such message: 99”, “message N was cancelled: sending was interrupted before it was saved”.

Times measured in the last recorded run (scenarios H2 and H3, 10 messages to 58 recipients on the test machine): rspeak list 0.24 s, rspeak status 0.28 s. In the prototype, before reading per user, rspeak list took about 6 s.

Chapter 11

Receiving in the terminal

11.1 · The shell hooks

Each shell has its own hook, installed where that shell reads it. They all do the same thing: if the mailbox contains messages, they run rootspeak-user prompt, right at login and then before every prompt.

ShellInstalled fileRead byWhen it asks
bash/usr/local/lib/rootspeak/rootspeak.bash/etc/profile.d/rootspeak.sh (login shells) and /etc/bash.bashrc (the others)at login and at every prompt (PROMPT_COMMAND)
zsh/usr/local/lib/rootspeak/rootspeak.zsha line in /etc/zsh/zshrc (or /etc/zshrc)at login and at every prompt (precmd)
fish/etc/fish/conf.d/rootspeak.fishfish itself, at startupat login and at every prompt (fish_prompt event)
sh, dash, ksh…/etc/profile.d/rootspeak.sh/etc/profile, in login shellsonly at login
Table 11.1 — The shell hooks

The message text reaches the terminal at send time whatever the shell: root writes it, not the shell (chapter 8.4). dash, ksh and similar shells have no place to hook into before the prompt: the question arrives at the next login, or through the desktop dialog (scenarios K of the test machine). profile.d/rootspeak.sh is written in plain sh because every shell reads it: it hands bash over to rootspeak.bash, leaves zsh to its own hook and asks the others at login.

i
Note. zsh reads the system-wide zshrc: if it is installed after RootSpeak, just run install.sh again to add the line. fish reads conf.d even if it arrives later.

11.2 · From the hook to the question

interactive bash$- contains iprofile.d/rootspeak.sh→ rootspeak.bash, oncePROMPT_COMMAND+ __rspeak_promptmailbox empty?compgen -Grootspeak-userprompt, only if needed
Figure 11.1 — From the hook to the question, in bash
etc/rootspeak.bash (excerpt)
__rspeak_prompt() {
	local s=$?
	# A check without external processes: with an empty mailbox it costs nothing.
	if compgen -G "$__RSPEAK_INBOX/[0-9]*" >/dev/null; then
		@LIBDIR@/rootspeak-user prompt
	fi
	return $s
}
  • The hook acts only in an interactive shell, if rootspeak-user exists, and once per shell (variable __RSPEAK_HOOK, in fish __rspeak_hook).
  • It adds __rspeak_prompt to PROMPT_COMMAND: as an element if it is an array (bash 5.1 and later), at the front if it is a string. It preserves $?: the user's prompt sees the result of their last command.
  • The mailbox check uses compgen -G, a bash builtin that starts no processes: with an empty mailbox the prompt costs nothing. zsh does the same with the (N) glob, fish with string match on the names (fish globs have no [0-9]).
  • In a login shell it calls rootspeak-user prompt --login straight away: the message appears before the first prompt (scenario C1b: about 0.3 s after login).
  • On Debian, interactive non-login shells (the desktop terminals) do not read /etc/profile.d: the hook is also called from /etc/bash.bashrc (chapter 3.3).

11.3 · rootspeak-user prompt

rootspeak-user prompt in src/rootspeak-user.c takes the messages in the mailbox in numeric order. If stdin is not a terminal it does nothing; the confirmation channel is the terminal name (terminal pts/3).

The rootspeak-user prompt looppending_ninbox in numeric orderrecently postponed?remindercounted, not askedshows and askstext, then [y/N]resultconfirm · postpone · …yes, not at loginnoat the end, if there are any:“You have N messagesfrom the administratorto confirm.”confirmed and expiredmove to read/
Figure 11.2 — rootspeak-user prompt, for each message in the mailbox
  • pending_n lists inbox/ (only names made of digits, and regular files). A message whose confirmation is already present, because of an interruption between the confirmation and the move, goes to read/ without asking; an expired one goes to read/ without a confirmation.
  • A message postponed less than RSPEAK_REMIND_MINUTES minutes ago (modification time of state/ID.term) is not asked about: it is counted, and at the end the one-line reminder appears. With --login the postponement does not apply.
Result of askWhat it doesPrints
0 · “y” or “Y”confirm(ID, "terminal pts/N")“[RootSpeak] Confirmed.”; or “[RootSpeak] The message has expired: the confirmation was not recorded.”, “[RootSpeak] The administrator has revoked the message.”, “[RootSpeak] The confirmation could not be recorded: you will be asked again.”
1 · anything else, or just Enterpostpone(ID, "term"): touches state/ID.term, event POSTPONED“[RootSpeak] You will be asked again in 30 minutes.”
2 · confirmed elsewheremoves to read/“[RootSpeak] Message already confirmed elsewhere (for example on the desktop).”
3 · revokednothing“[RootSpeak] The administrator has revoked the message.”
4 · expired while the question was openmoves to read/“[RootSpeak] The message has expired.”
Table 11.2 — The results of the question
The question at the prompt, answered with y
mario@server:~$ 
── RootSpeak · Message from the administrator · Maintenance · 01/10 10:49 ──
On Saturday from 8 am to 12 noon the server will be off for disk maintenance.
────────────────────────────────────────
[RootSpeak] Do you confirm you have read the message? [y/N] y
[RootSpeak] Confirmed.
mario@server:~$ 
i
Note. rootspeak-user runs inside the user's prompt: it never ends with an error that would disrupt the shell, and any problem it meets (unreadable mailbox, no terminal) turns into doing nothing.

11.4 · The question and the wait

ask waits for the answer on /dev/tty with poll, one second at a time, then reads it with read. At every second without an answer it checks the mailbox: if the confirmation has appeared, the user confirmed elsewhere; if the message has disappeared, it was revoked; if it has expired, the question stops.

A question that checks every secondUserkeyboardaskpoll 1 s on /dev/ttyMailboxusers/U[RootSpeak] Do you confirm…? [y/N]waits at most 1 second acks/ID? inbox/ID? expired?nothing new: starts over meanwhile: user confirms on desktop acks/ID existsresult 2: “confirmed elsewhere”
Figure 11.3 — The question notices what happens elsewhere
  • The terminal stays in canonical mode: whatever the user is typing stays in the terminal's buffer and is not lost from one second to the next.
  • The question closes within about a second: confirmation from another session (scenario C4b, 0.95 s), revocation (scenario E3, 1.0 s), expiry (tests/engine.sh, group 4).
  • Without /dev/tty (no controlling terminal) the answer counts as “no”: the message stays and comes back later.
  • Only the first character of the answer is checked: only y and Y mean yes. RootSpeak speaks English, whatever the language of the session.
Chapter 12

Receiving on the desktop

12.1 · The agent's loop

The agent, rootspeak-user agent, starts with the desktop's autostart (/etc/xdg/autostart/rootspeak-agent.desktop) or from systemd-run when rspeak send does not find one, and lives as long as the graphical session.

The agent's loopflock LOCK_NBone agent per userrootspeak-agent.readythe PID, for wake-upsgraphical session?sd-login, every roundfor each messagenot postponedwaitppoll 1 send of wait: RSPEAK_AGENT_POLL seconds or SIGUSR1no: the agent exitslock taken:exits at once
Figure 12.1 — The desktop agent's loop
  • Only one agent per user: flock(LOCK_EX | LOCK_NB) on $XDG_RUNTIME_DIR/rootspeak-agent.lock; if the lock is taken the agent exits at once, without an error. The descriptor is opened with O_CLOEXEC and is not passed on to zenity: otherwise, if the agent died, one of its orphaned dialogs would hold the lock and the agent started in its place would exit at once (a defect found by the test VM).
  • Ready: after taking the lock the agent writes its PID to $XDG_RUNTIME_DIR/rootspeak-agent.ready and deletes it on exit (atexit). rspeak send wakes only that PID (chapter 8.5).
  • Lives as long as the session: the loop goes on as long as graphical_session(getuid()) finds an X11 or Wayland session of the user, or until a termination signal arrives.
  • Configuration always current: conf_read at every round; a change to RSPEAK_REMIND_MINUTES or RSPEAK_AGENT_POLL applies from the next round.
  • One dialog at a time: the messages not postponed on the desktop channel (state/ID.gui), oldest first (scenario I7).

12.2 · Signals and waits without losses

The agent must react to three things: a new message (SIGUSR1 from rspeak send), the end of the session (SIGTERM from logind) and the closing of the dialog (SIGCHLD). It handles them without losing signals and without interrupting a confirmation halfway.

No signal lostalways blockedUSR1 TERM HUP INT CHLDppoll(…, &waiting)unblocked only hereSIGUSR1wake = 1SIGTERM, HUP, INTfine = 1SIGCHLDzenity has closednew round nownew messageexitsat the first safe pointrecordsthe answer, at oncea signal that arrives outside the wait stays pending: it is not lost
Figure 12.2 — The agent's signals: blocked except during the waits
SignalHandlerEffect
SIGUSR1on_wake: wake = 1the wait ends and the round starts again at once; it is installed as the first statement of main, because by default SIGUSR1 terminates the process (in the prototype, 20 out of 20 agents woken while starting up died)
SIGTERM, SIGHUP, SIGINTon_end: fine = 1the agent finishes recording an answer already given and exits at the first safe point
SIGCHLDon_child: nothinginterrupts the wait: the dialog's answer is recorded within a few milliseconds
Table 12.1 — The agent's signals
  • The five signals stay blocked with sigprocmask all the time, except during the waits, done with ppoll, which unblocks and re-blocks them atomically: a signal that arrives while the agent is working stays pending and interrupts the next wait.
  • Each wait lasts at most one second; the one between two rounds lasts RSPEAK_AGENT_POLL seconds (60) in total, and ends earlier if a wake-up or termination arrives. The periodic check stays as a safety net: a lost signal delays the dialog, it does not lose it.
  • The wake-up clears its flag at the start of the round: a wake-up that arrives while a dialog is open makes the next round start at once.

12.3 · The confirmation dialog

show_dialog starts zenity --question --no-markup --width=460 with title, text and buttons in English, whatever the language of the session, and waits for it to finish, checking the mailbox every second as well.

Figure 12.3 — The dialog as zenity draws it on GNOME (reconstruction), with the buttons “Later” and “I have read it”
A dialog, from opening to answershow_dialogagentzenity--question --no-markupUserMailboxusers/Ufork, exec (signals restored)“I have read it” / “Later”every second: waitpidacks/ID? inbox/ID? expired?three ways to end “I have read it”: exit 0SIGCHLD → desktop confirmation“Later”, closed: exit 1postponement: state/ID.guielsewhere/revoked: SIGTERM
Figure 12.4 — The confirmation dialog
How it endsEffect
exit 0 (“I have read it”)confirm(ID, "desktop"); if writing fails and the message is still in the mailbox, a notification says so and the dialog comes back at the next round
exit 1 (“Later”, or dialog closed by the user)postpone(ID, "gui"): touches state/ID.gui, event POSTPONED; only if the display is still there: zenity also exits with 1 when it loses the display (session ended, display manager restarted)
closed by the agent: confirmation elsewhere, revocation or expirynothing: if it has expired, the message moves to read/
other results (dialog killed, for example when the session ends)nothing: it is not the user's choice; the dialog comes back at the next round or at the next login
Table 12.2 — How a dialog ends
  • --no-markup: the message text is not interpreted as zenity markup. The text has already been cleaned at send time (chapter 8.7).
  • The child that becomes zenity restores the signal mask and handlers before exec: zenity does not inherit the agent's blocked signals.
  • The end of zenity interrupts the wait at once (SIGCHLD): the answer is recorded within a few milliseconds, so an rspeak remind arriving right after a “Later” finds the postponement already written and removes it (scenario I11; with a check once a second, the VM had found the reminder lost).
  • The wake-up signal does not close the dialog: the agent keeps waiting for it. In the bash prototype the signal interrupted wait and the agent took the dialog as closed, recording a postponement never chosen (test VM, scenario I7).
  • When the session ends the agent waits for the dialog at most 2 seconds, then closes it: a confirmation already given is not lost (scenario I10: logout as soon as zenity has exited with the answer, confirmation recorded).
  • After an exit 1 the agent tells “Later” from a lost display: the session must still be there for logind (not closing) and the display socket ($WAYLAND_DISPLAY or /tmp/.X11-unix/XN) must accept a connection. Otherwise nothing is recorded and the dialog comes back at the next login (scenario I13). Until 1 October 2026 the end of the session was recorded as a postponement, and the dialog came back only after RSPEAK_REMIND_MINUTES: the tests on Ubuntu, Fedora with KDE Plasma and Cinnamon found it.
  • Timings of the last run recorded in the VM: dialog after sending 0.58 s (I1), closing after a confirmation from the terminal 0.65 s (I5), after a revocation 0.67 s (I6), at graphical login 2.64 s after the login manager restart (I9).

12.4 · The error notification

The agent uses notify-send, if it is installed, for one case only: telling the user that their confirmation has not been recorded (disk full, permissions). The text is fixed: no message content goes through notifications, which may disappear by themselves or be ignored.

Figure 12.5 — The only notification of RootSpeak (reconstruction)
i
Note. for the confirmation RootSpeak uses a dialog and not a notification: a GNOME notification may disappear by itself or be ignored, whereas the zenity dialog stays open until the user chooses.
Chapter 13

Confirmations

13.1 · The first one counts

confirm(ID, CHANNEL) in src/rootspeak-user.c is the only place where a confirmation is born. It writes it to a temporary file, flushes it to disk and publishes it with link, which fails if acks/ID already exists: the confirmation is born whole or not at all, and “already confirmed” stays distinct from “write failed”.

How a confirmation is bornacks/ID there?result 0the first one countsinbox/ID there?result 3revokedexpired?result 2to read/, no confirmationacks/.ID.PIDdate channel · fsynclink(tmp, acks/ID)fails if it already existsfsync(acks/), read/event CONFIRMEDresult 1 or 0failed, or it appeared meanwhileyesnonoyesyesnosucceedserror
Figure 13.1 — confirm(ID, CHANNEL): the checks and the exclusive publication
src/rootspeak-user.c (in short)
static int confirm(long long id, const char *channel)
{
	if (exists(ack)) { mark_read(id); return 0; }          /* the first one counts */
	if (!has_entry("inbox", id, "")) return 3;             /* revoked: no confirmation */
	if (expired(id)) { mark_read(id); return 2; }
	fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0644);
	if (fd < 0 || n <= 0 || write_all(fd, line, (size_t)n) < 0 || fsync(fd) < 0 || close(fd) < 0 ||
	    link(tmp, ack) < 0) {                              /* link fails if acks/ID exists */
		unlink(tmp);
		if (exists(ack)) { mark_read(id); return 0; }  /* another confirmation came first */
		event("CONFIRMATION_FAILED", id, U, channel, err, "");
		return 1;                                      /* the message stays in the mailbox */
	}
	unlink(tmp);
	sync_path(dir);                                        /* on disk before “Confirmed” */
	mark_read(id);
	event("CONFIRMED", id, U, channel, "", "");
	return 0;
}
CaseResultWhat happens
confirmation written0the message moves to read/, the postponements are removed; event in the log
acks/ID already present0moves to read/; the first confirmation stays
write failed (disk full, permissions)1stays in inbox/; the user is told, the error goes to the log, the question or the dialog comes back (tests/engine.sh, group 1; scenario F4)
message expired2moves to read/ without a confirmation: a confirmation after the expiry does not count
copy gone (revoked)3no confirmation
Table 13.1 — The results of confirm
  • The line is date channel: seconds since the Unix epoch, a space, the channel as it is shown (desktop, terminal pts/3, terminal tty3).
  • The temporary file acks/.ID.PID starts with a dot: it is never mistaken for a confirmation. If an interruption leaves it behind, it is ignored.
  • If the revocation arrives between the check of inbox/ID and link, the confirmation is born, but rspeak status does not count it: its date is later than sent/ID/revoked (chapter 10.1).
  • mark_read renames inbox/ID to read/ID and removes the postponements. If it is interrupted between the confirmation and the move, whoever reads the mailbox next completes the move without asking again (tests/engine.sh, group 1).

13.2 · Two simultaneous confirmations

A user may have the dialog open on the desktop and the question in two terminals. The confirmation must be one: the first to arrive.

The first confirmation winsDialogconfirm … desktopacks/IDlink: exclusiveTerminalconfirm … pts/3link: succeedslink: fails, already existsexists(ack): mark_read, code 0 CONFIRMED (desktop)a second later, the other open questionacks/ID appeared?“already confirmed elsewhere”
Figure 13.2 — Two almost simultaneous confirmations: the first one counts
  • Two almost simultaneous confirmations produce a single record: link succeeds only once; the other finds acks/ID and only moves the message.
  • The questions and dialogs still open notice it at their next check (within a second) and close: “[RootSpeak] Message already confirmed elsewhere (for example on the desktop).” in the terminal, while the dialog closes by itself (scenarios C4b and I5).
  • This rule came from a live test of the prototype: the user had to confirm twice, from the dialog and from the terminal.

13.3 · The confirmations read by root

rspeak status reads the confirmations from the mailbox, which the user controls. That is why it treats them as untrusted data, and their meaning is deliberately limited.

RiskRule
a symbolic link acks/ID pointing to a restricted filethe read happens as the user (chapter 7): a file the user cannot read is not read
a pipe or a device in place of the confirmationonly a regular file is read, opened without blocking
a huge confirmationat most 200 bytes are read, and only the first line
escape sequences in the channelthe output goes through clean_text (scenario G3)
a bogus dateit is just a number: it counts only if it is not later than the expiry and revocation in sent/
Table 13.2 — The confirmations read by root
i
Note. “Confirmed” means: the mailbox holds a valid declaration of reading by the user. It is not an action observed by RootSpeak nor a proof of reading: the user controls their own mailbox and could write it themselves, just as they could press “I have read it” without reading. Whoever uses rspeak status for an audit must read it this way (chapter 19.1).
Chapter 14

The TUI for administrators

14.1 · Architecture: same data, same commands

rspeak without arguments, with stdin and stdout on a terminal of at least 80 × 20, opens the TUI of src/tui.c after recovery; in a pipe, or on a smaller terminal, it shows the help. The TUI has no way of its own of doing things and no states of its own: it reads the store with the functions of rspeak list and rspeak status, and every action is the command itself.

No way of its ownThe TUI · src/tui.clistload_list → summarisedetailrecipients · historynew messageform, Ctrl+E, Ctrl+SSame functions as rspeaksummariselike rspeak listrecipientslike rspeak statusmailboxes_emptyat every refreshIn a child processcmd_sendcmd_revoke · cmd_remindcmd_purgecmd_log · recipient countactionsoutput and errors collected on a pipe and shown in a window
Figure 14.1 — The TUI reads with the commands' functions and runs the commands themselves
  • Same functions for reading: the list uses summarise (the recipients counted in the three states, revoked and expired), the detail uses recipients (the same line as rspeak status).
  • The actions are the commands: sending, revoking, reminding and purging run in a child process that calls cmd_send, cmd_revoke, cmd_remind, cmd_purge with stdout and stderr collected on a pipe and shown in a window. An error of the command (die) ends only the child; the child's exit status gives the result. The recipients preview and the history (cmd_log) also come from a child.
  • The decision of the user who wanted the TUI is also a design constraint: while discussing the previews, invented states had appeared (“pending”, “complete”, “incomplete”, “cancelled”) and were removed; tests/tui.py checks that none of these words appears on the screen.

14.2 · The list

The list shows the messages from the most recent, one per row, with the recipients summary: a three-colour bar and a phrase. It refreshes by itself every 3 seconds.

┌─ RootSpeak · sent messages ──────────────────────────────────────────────────────────────── updated at 11:20:20 ─┐
│                                                                                                                  │
│   ID  DATE         MESSAGE                              RECIPIENTS                                               │
│    4  01/10 11:20  [revoked] Reboot · Reboot at 1 pm.   ▒▒▒▒▒▒▒▒▒▒▒▒  2 delivered                                │
│    3  01/10 11:20  Your account expires on Friday: ple  ▒▒▒▒▒▒▒▒▒▒▒▒  1 delivered                                │
│ >  2  01/10 11:20  Maintenance · On Saturday from 8 am  ██████▒▒▒▒▒░  4 confirmed · 3 delivered · 1 sent         │
│    1  01/10 11:20  Server update · Tonight at 11 pm th  ████████████  all confirmed (3)                          │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│ █ confirmed   ▒ delivered   ░ sent                                                                               │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Enter open   n new   r revoke   m remind   p purge   / search   ? keys   q quit                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Figure 14.2 — The message list (real screenshot, captured by tools/tui-screenshots.sh)
ColumnContent
ID, DATEnumber and date of sending; > marks the selected row, drawn in reverse video
MESSAGE[revoked] and [expired] in grey, the title in bold, the first line of the text, cut to the available width
RECIPIENTSthe 12-cell bar and the summary phrase (chapter 10.3); below 106 inner columns, instead of the phrase, “confirmed/total” (for example 4/8)
legendalways at the bottom: █ confirmed, ▒ delivered, ░ sent; with an active search, the searched text
Table 14.1 — The columns of the list
KeyEffect
↑ ↓ PgUp PgDn Home Endscrolls the list
Enteropens the detail of the selected message
nwrites and sends a new message
rrevokes the selected message, if it is not already revoked (with confirmation)
mreminds whoever has not confirmed: only if the message is neither revoked nor expired and has at least one delivered recipient (with confirmation)
pdeletes the messages older than a duration (with confirmation; suggested 90d)
/searches by text or title, case-insensitively; Esc clears the search
?the key guide
qquits and gives the terminal back as it was
Table 14.2 — The keys of the list

14.3 · The detail and the history

The detail of a message has two tabs: the recipients, with the status line of rspeak status coloured according to the state, and the history, that is the output of rspeak log. It is re-read every 3 seconds and after every action; Esc leaves it.

┌─ Message 2 · Maintenance ────────────────────────────────────────────────────────────────── updated at 11:20:20 ─┐
│  Maintenance · 01/10 11:20 · from root                                                                           │
│  │ On Saturday from 8 am to 12 noon the server will be off for disk maintenance.                                 │
│  │ Please save your work by Friday evening.                                                                      │
│                                                                                                                  │
│   Recipients (8)   History     ██████▒▒▒▒▒░  4 confirmed · 3 delivered · 1 sent                                  │
│                                                                                                                  │
│  USER          STATE                                                                                             │
│  admin         confirmed on 01/10 11:20 (terminal pts/0)                                                         │
│  anna          confirmed on 01/10 11:20 (desktop)                                                                │
│  dario         sent, not delivered: the copy could not be written (see the log; the next rspeak command will ret │
│  fabio         confirmed on 01/10 11:20 (desktop)                                                                │
│  kora          delivered, postponed on 01/10 11:20                                                               │
│  luca          delivered                                                                                         │
│  mario         delivered, postponed on 01/10 11:20                                                               │
│  zeno          confirmed on 01/10 11:20 (terminal pts/2)                                                         │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Tab recipients/history   ↑/↓ scroll   m remind   r revoke   Esc back                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Figure 14.3 — The detail: one recipient per row, in the three states (real screenshot)
┌─ Message 2 · Maintenance ────────────────────────────────────────────────────────────────── updated at 11:20:26 ─┐
│  Maintenance · 01/10 11:20 · from root                                                                           │
│  │ On Saturday from 8 am to 12 noon the server will be off for disk maintenance.                                 │
│  │ Please save your work by Friday evening.                                                                      │
│                                                                                                                  │
│   Recipients (8)   History     ██████▒▒▒▒▒░  4 confirmed · 3 delivered · 1 sent                                  │
│                                                                                                                  │
│  2026-10-01 11:20:19  message 2 delivered to admin (terminals written: 0)                                        │
│  2026-10-01 11:20:19  message 2 delivered to anna (terminals written: 0)                                         │
│  2026-10-01 11:20:19  message 2: ERROR, copy for dario not written                                               │
│  2026-10-01 11:20:19  message 2 delivered to fabio (terminals written: 0)                                        │
│  2026-10-01 11:20:19  message 2 delivered to kora (terminals written: 0)                                         │
│  2026-10-01 11:20:19  message 2 delivered to luca (terminals written: 0)                                         │
│  2026-10-01 11:20:19  message 2 delivered to mario (terminals written: 0)                                        │
│  2026-10-01 11:20:19  message 2 delivered to zeno (terminals written: 0)                                         │
│  2026-10-01 11:20:19  message 2 sent by root to: admin anna dario fabio kora luca mario zeno                     │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                            
Figure 14.4 — The History tab: the log events, rewritten from the RSPEAK_* fields (real screenshot)
  • The header shows title, date, sender, expiry (“expires on …” or “expired on …”) and revocation (“revoked on …”, in red); then at most six lines of the text, indented.
  • The tab row repeats bar and phrase: the summary stays visible while scrolling through the list.
  • Colours of the STATE column: green confirmed, yellow delivered, red sent. They are the three states, and only those.
  • Keys: Tab switches tab, ↑ ↓ scroll, m and r remind and revoke with the same rules as the list.

14.4 · Writing and sending

The new-message form has four fields: recipients, title, expiry and text. When the recipients field is left, a child performs the same resolution as rspeak send and shows the number of recipients, or the error in red.

┌─ New message ────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                                                  │
│  To         [ @developers                                                              ]  2 recipients           │
│  Title      [ Maintenance                                                              ]  optional, one line     │
│  Expires    [ 2d                                                                       ]  optional: 30m, 2h, 3d  │
│  Text       ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│             │ On Saturday from 8 am to 12 noon the server will be off.                                         │ │
│             │ Please save your work by Friday evening.                                                         │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             └──────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│             97 of 65536 bytes                                                                                    │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Tab next field   Ctrl+E open in the editor   Ctrl+S send   Esc cancel                                            │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Figure 14.5 — The new-message form, with the recipients preview (real screenshot)
┌─ New message ────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                                                  │
│  To         [ @developers                                                              ]  2 recipients           │
│  Title      [ Maintenance                                                              ]  optional, one line     │
│  Expires    [ 2d                                                                       ]  optional: 30m, 2h, 3d  │
│  Text       ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│             │ On Saturday from 8 am to 12 noon the server will be off.                                         │ │
│             │ Please save your work by Friday evening.                                                         │ │
│             │                                                                                                  │ │
│                           ┌─ Send the message? ──────────────────────────────────────┐                           │
│                           │                                                          │                           │
│                           │  To: @developers (2 recipients)                          │                           │
│                           │  Title: Maintenance                                      │                           │
│                           │  Expires: 2d                                             │                           │
│                           │  Text: 97 bytes                                          │                           │
│                           │                                                          │                           │
│                           │   y  Confirm       n  Cancel                             │                           │
│                           └──────────────────────────────────────────────────────────┘                           │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             │                                                                                                  │ │
│             └──────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│             97 of 65536 bytes                                                                                    │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ y confirm   n or Esc cancel                                                                                      │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Figure 14.6 — The confirmation before sending (real screenshot)
KeyEffect
Tab, Shift+Tabnext or previous field
Ctrl+Eopens the text in the administrator's editor
Ctrl+Sasks for confirmation and sends: cmd_send in a child, with the text on stdin
Escdiscards the message, with confirmation if something has been written
Table 14.3 — The keys of the form
  • One-line fields accept at most 1000 bytes; the text shows the count “97 of 65536 bytes”. The real checks are those of rspeak send: an error arrives in the result window, “Sending result”.
  • Editor: Ctrl+E writes the text to a temporary file in /tmp (mkstemp), hands it over to the administrator in SUDO_USER and opens VISUAL, EDITOR, nano or vi with their identity, not as root; then it re-reads the file (at most twice the maximum length, without following links) and deletes it.
  • After sending, the list reloads and the new message appears at the top (tests/tui.py).
i
Note. a defect found while writing tests/tui.py: the child that performs the send also inherited the end of the pipe through which the parent writes the text, and never saw the end of the text. Now it closes it before calling cmd_send.

14.5 · The actions with confirmation

Revoking, reminding and purging always ask for a confirmation that describes the action with the real numbers; the result of the command then appears in a window.

┌─ RootSpeak · sent messages ──────────────────────────────────────────────────────────────── updated at 11:20:26 ─┐
│                                                                                                                  │
│   ID  DATE         MESSAGE                              RECIPIENTS                                               │
│    4  01/10 11:20  [revoked] Reboot · Reboot at 1 pm.   ▒▒▒▒▒▒▒▒▒▒▒▒  2 delivered                                │
│    3  01/10 11:20  Your account expires on Friday: ple  ▒▒▒▒▒▒▒▒▒▒▒▒  1 delivered                                │
│ >  2  01/10 11:20  Maintenance · On Saturday from 8 am  ██████▒▒▒▒▒░  4 confirmed · 3 delivered · 1 sent         │
│    1  01/10 11:20  Server update · Tonight at 11 pm th  ████████████  all confirmed (3)                          │
│                                                                                                                  │
│                                                                                                                  │
│                           ┌─ Remind message 2? ──────────────────────────────────────┐                           │
│                           │                                                          │                           │
│                           │  3 recipients have not confirmed yet.                    │                           │
│                           │                                                          │                           │
│                           │  They are asked again now: at the next prompt            │                           │
│                           │  and with the dialog on the desktop.                     │                           │
│                           │                                                          │                           │
│                           │   y  Confirm       n  Cancel                             │                           │
│                           └──────────────────────────────────────────────────────────┘                           │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│                                                                                                                  │
│ █ confirmed   ▒ delivered   ░ sent                                                                               │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ y confirm   n or Esc cancel                                                                                      │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Figure 14.7 — The confirmation of a reminder, over the list (real screenshot)
ActionThe confirmation saysThen
revoke“Revoke message N?”, title and summary; “Whoever has not seen it yet will not see it; open dialogs and questions close by themselves.”cmd_revoke in a child
remindhow many recipients have not confirmed yet; “They are asked again now: at the next prompt and with the dialog on the desktop.”cmd_remind in a child
purgefirst the duration (“Delete the messages sent more than (for example 90d, 12h):”), then “Messages older than D, with copies and confirmations. The events stay in the system log.”cmd_purge in a child
sendrecipients with their number, title, expiry, bytes of the textcmd_send in a child
Table 14.4 — The actions with confirmation

Windows and confirmations are drawn on a copy of the base screen: a later window does not stay on top of the previous one. y confirms, n or Esc cancels.

14.6 · Terminal, keys and refresh

PartHow it works
terminal/dev/tty without echo or canonical mode; without IXON (so that Ctrl+S reaches the TUI) and without ICRNL; alternate screen and hidden cursor. On exit, and on SIGINT, SIGTERM, SIGHUP, everything goes back to how it was
drawingwithout ncurses: every screen is composed in memory and written with a single write; lengths are counted in visible characters (UTF-8, without the colour sequences)
keysone byte at a time with a poll of 500 ms; after ESC the rest of the sequence with a 40 ms wait: Esc on its own, arrows, PgUp, PgDn, Home, End, Del and Shift+Tab are recognised
refreshevery 6 empty waits (3 seconds) the list or the detail is re-read, forgetting the mailboxes already read (mailboxes_empty); at the top right “updated at …”
sizeat every wait the size is compared (TIOCGWINSZ) and the screen redrawn; below 80 × 20 the TUI exits
coloursturned off with NO_COLOR (if sudo preserves it)
Table 14.5 — Terminal, keys and refresh
!
Warning. the TUI runs as root. Everything it shows that comes from the users (confirmations, history) goes through clean_text as in the commands; the only external program the TUI starts on its own, the editor, runs with the administrator's identity.
Chapter 15

The built-in help

15.1 · A single source

The built-in help of rspeak --help is modelled on that of MTERM: no system manual pages, but a tabbed help in the terminal. Its texts live in src/help.c, in English, as "label¦description" items, one array per group, and are shown as they are.

Where the texts come fromsrc/help.citems “label¦text”tabbed helptabbed_helppage with pagerpage(), in colourplain textpage(), 80 columnsbuild.pyhelp_itemsUserManualtools/check-docs.pygenerated manual = src/help.cread from the source
Figure 15.1 — A single source for the help, the page, the text and the manual's reference
ArrayContentIn the user manual
COMMANDSthe commands: send, list, status, revoke, remind, log, purge, version, help and rspeak on its ownreference, with data-help
OPTIONS--to, --title, --expires, --filereference, with data-help
EXAMPLESexample commands with their explanationthe table of examples
FILESconfiguration, message store, log, manual—
SYNOPSISthe synopsis lines—
Table 15.1 — The groups of help texts

The manual generator reads the items straight from the arrays of src/help.c (help_items in docs/sources/build.py), in code order, and builds from them the user manual's reference, with the data-help attribute on every entry; tools/check-docs.py reads src/help.c again at check time and verifies that commands and options match the manual, word for word (chapter 20.5).

15.2 · The three forms

rspeak_help chooses the form according to where the output goes. All three have the same content.

Three formsstdin and stdout tty?at least 44 × 10?tabbed helptabbed_helpplain text, 80 columnsno coloursmanual-style page$PAGER or less -R -F -Xyesyesnonoshrunk below 44 × 10:the help exits
Figure 15.2 — Which form rspeak --help takes
FormWhenHow
tabbed helpstdin and stdout on a terminal of at least 44 × 10five tabs: Info, Commands, Options, Examples, Files
manual-style pagesmaller terminalthe page (NAME, SYNOPSIS, DESCRIPTION, COMMANDS, SEND OPTIONS, EXAMPLES, FILES, ENVIRONMENT, EXIT STATUS, SEE ALSO) as wide as the terminal, between 60 and 100 columns, through $PAGER or less -R -F -X if it is in the PATH; otherwise directly
plain textstdin or stdout is not a terminal (a pipe, a file)the same page, 80 columns, without colours
Table 15.2 — The three forms of the help
  • Colours are turned off with NO_COLOR; PAGER and NO_COLOR reach rspeak only if sudo preserves them (env_keep in sudoers): the help says so in the ENVIRONMENT section.
  • The help too requires root: rspeak --help re-runs itself with sudo like every other command (chapter 4.1). It is the user's decision, at the cost of asking for the password even for the help.
  • rspeak on its own in a pipe shows the help as plain text (tests/tui.py).
The help in a pipe (beginning)
admin@server:~$ sudo rspeak --help | head -5
RSPEAK(1)                        rspeak manual                         RSPEAK(1)

NAME
       rspeak — sends messages to the users of the machine and tracks their read
       confirmation

15.3 · The tabbed help

PartHow it works
terminal/dev/tty without echo or canonical mode (termios), alternate screen and hidden cursor; on exit and on SIGINT, SIGTERM, SIGHUP everything goes back to how it was
keysone byte at a time with a poll of 0.3 s; after ESC the rest with a wait of 50 ms: Esc on its own quits; arrows, PgUp, PgDn, Home, End are recognised
tabs← →, Tab, Shift+Tab, h l, Enter, or the digits 1…5
scrolling↑ ↓ (k j), PgUp PgDn (b Space), g G; each tab remembers its position; at the bottom “lines 1-20/57”
resizingat every 0.3 s wait the help compares the size (TIOCGWINSZ) with the last one and redraws; below 44 × 10 it exits
drawingthe tab's lines are prepared without colours, so the lengths are the visible ones; frame, tabs and status line are written in a single write; the status line is shortened if it does not fit
exitq or Esc: main screen restored, exit code 0
Table 15.3 — The tabbed help

tests/help-tui.py opens the help on a 100 × 30 pseudo-terminal, presses keys like a person and checks every screen: opening and frame, tab switching with arrow and digit, end and start of a tab, resizing to 70 × 20, clean exit, and the page with the pager on a 40 × 8 terminal (chapter 20.4).

Chapter 16

Security

16.1 · The trust model

Administrators are trusted: they can already do anything on the machine, and RootSpeak does not protect them from themselves. Users are not trusted: they control their own mailbox and their own terminals, and they can write to the journal. Everything that goes from root to a user, or comes back from a user to root, crosses a controlled boundary.

Who is trustedTrusted: administratorsrspeak (root)sudo, admin group, 750sent/ · rootspeak.confonly root writesBoundaryas_userchild with U's identityclean_text, limitstexts, 200 bytes, timesUntrusted: usersusers/U/mailbox: U can do anythingacks/ID · state/ID.*written by the usereverything that crossesthe boundary goes through here
Figure 16.1 — The RootSpeak trust model
DirectionWhat crossesControl
root → mailboxcopies, revocations, reminders, deletionsalways as the user, with a time limit (chapter 7)
root → terminalthe message textcleaned; owner of the opened terminal; 3 s at most (chapter 8.4)
root → agentSIGUSR1from a child running as the user, only to the ready PID with the exact command line
mailbox → rootconfirmations and postponement datesread as the user, 200 bytes, regular files only, cleaned (chapter 10.2)
journal → rootevents with the RSPEAK_* fields_UID compared with who was supposed to write them; states never taken from the journal (chapter 17.4)
Table 16.1 — The boundary, direction by direction

16.2 · Risks and countermeasures

RiskCountermeasureTest
an ordinary user runs rspeakpermissions 750 root:<admin group>; re-launch with sudo, which applies its own rulesscenarios A3, A4
RSPEAK_TEST used to bypass sudoit grants nothing: without root a user keeps their own permissions; the test variables apply only in test modescenario G4
symbolic links in the mailboxevery access by root to the mailboxes happens in a child with the user's identity, with a time limitscenario G1
abnormal mailboxes (pipes, thousands of files) against root's commandsonly regular files and only existing messages are read, with a time limitengine.sh, group 9
escape sequences in messagesclean_text on text and title; --no-markup in zenityscenario G2
forged confirmationsread as the user, at most 200 bytes, cleaned; a confirmation remains a statement by the user, not a proofscenario G3
fake events in the journalthe journal adds _UID; rspeak log flags as “not trustworthy” an event written by the wrong user; states are read from the message storescenario J3
text meant for one user written to another user's terminalthe owner of the terminal is checked after opening it, not the name /dev/pts/Nengine.sh, group 9
signals to the wrong processesthe signal is sent from a child with the user's identity, and only to the ready PID with the agent's exact command lineengine.sh, group 7
a stuck terminal halts sendingnon-blocking write, at most 3 s per terminal—
a user stops root's child (SIGSTOP)time limit even after the output has ended, then SIGKILL—
configuration as coderootspeak.conf is a data file: two keys, positive integersengine.sh, group 5
an editor opened as root from the TUIthe editor runs with the identity of the administrator in SUDO_USER—
text too long for the dialogat most 65536 bytes: zenity receives the text as an argument, and Linux does not accept arguments over 128 KiBengine.sh, group 9
messages read by other usersmailboxes 700; root's sent/ 700—
memory errors in a root programcompiler hardening, static analysis, sanitizers, fuzzing (chapter 16.3)chapter 20.7
Table 16.2 — Risks and countermeasures
i
Note. outside the model: a symbolic link /var/lib/rootspeak created before installation. Only root writes in /var/lib, and root is already trusted (second review, finding not accepted).

16.3 · Compiler hardening

A root program in C that reads user data must not have memory errors. Besides the coding rules (buf_t for every text that comes from outside, no variable-length arrays, O_CLOEXEC everywhere), the code is compiled with the hardening that gcc and the linker offer.

OptionWhat it does
-Wall -Wextra -Wpedantic … -Werrorall the useful warnings, as errors: among others formats (-Wformat=2, -Wformat-security), shadowed names (-Wshadow), null pointers (-Wnull-dereference), variable-length arrays (-Wvla)
-D_FORTIFY_SOURCE=3size checks in library functions (copies, formats)
-fstack-protector-stronga stack canary in functions with arrays or addresses of local variables
-fstack-clash-protectionthe stack cannot jump past the guard page
-fcf-protectioncontrol-flow protection (where the processor provides it)
-fPIE -pieposition-independent executable at random addresses
-z relro -z nowlinkage tables read-only after startup
-z noexecstacknon-executable stack
-fanalyzer (make analyze)gcc static analysis: error paths, descriptors not closed, use after free
-fsanitize=address,undefined (make sanitizer)AddressSanitizer and UBSan in the tests: any out-of-bounds access or undefined behaviour stops the test
Table 16.3 — Compiler hardening (Makefile)
  • Fuzzing with libFuzzer (tests/fuzz/) exercises, with AddressSanitizer and UBSan, everything that reads untrusted data, with 4 targets: text cleaning, message format, the output of mailbox reading, configuration.
  • The choice of C, rather than a language that avoids memory errors by construction, is the owner's: the hardening in this table is the condition under which it was made (chapter 21.3).
Chapter 17

The event log

17.1 · Structured events in the journal

Every relevant fact in the life of a message becomes a structured event in the system journal. The log answers a different question from rspeak status: not “where does it stand” but “what happened, when, and by whose hand”.

The logrspeak (root)send, revoke, remind…rootspeak-user (U)postpone, confirmevent()src/event.cjournalsd_journal_sendsyslogif the journal is missingjournalctl-t rspeakrspeak log IDand the TUI historyreadable text in English, plus the RSPEAK_* fields; the journal adds _UID
Figure 17.1 — From events to the log, and back
  • event(EV, ID, USER, CHANNEL, DETAILS, AUTHOR) in src/event.c writes with sd_journal_send a readable text (MESSAGE) and the structured fields. If the journal does not respond, it writes at least the text with syslog.
  • The readable text is in English, whatever the language of the system; rspeak log rewrites it from the fields with the same function, event_text.
  • The details fit on a single line: line breaks become spaces.
  • The log is the history, not the state: states are always derived from the message store, never from the journal (chapter 19.1). Retention of the events follows the journal's rules, not those of RootSpeak: rspeak purge does not touch them.

17.2 · The fields of an event

FieldContentSet by
SYSLOG_IDENTIFIERrspeak, for all events, including those of rootspeak-userRootSpeak
MESSAGEthe readable text, in EnglishRootSpeak
PRIORITY6 (informational)RootSpeak
RSPEAK_EVENTthe name of the event: SENT, CONFIRMED…RootSpeak
RSPEAK_MESSAGEthe message numberRootSpeak
RSPEAK_USERthe recipient, where relevantRootSpeak
RSPEAK_CHANNELterminal pts/N, desktop; for postponements terminal or desktopRootSpeak
RSPEAK_DETAILSrecipients, terminals written, number reminded or recovered, duration, errorRootSpeak
RSPEAK_AUTHORthe administrator (SUDO_USER)RootSpeak
_UID, _PID, timewho actually wrote the event, and whenthe journal
Table 17.1 — The fields of an event

For an audit, journalctl RSPEAK_MESSAGE=12 -o verbose shows all the fields, _UID included; journalctl -t rspeak shows the text of all RootSpeak events.

17.3 · The events

EventWritten byWhenText
SENTrspeak (root)at the end of rspeak send, after the deliveriesmessage 12 sent by admin to: anna mario
DELIVEREDrspeak (root)for every copy written and verified; also by recoverymessage 12 delivered to mario (terminals written: 1) · message 12 delivered to mario (recovery)
NOT_DELIVEREDrspeak (root)copy not writtenmessage 12: ERROR, copy for anna not written
RECOVEREDrspeak (root)recovery of an interrupted sendingmessage 12: delivery completed for 3 recipients after an interruption
CANCELLEDrspeak (root)recovery of a sending that was not savedmessage 12: sending interrupted before it was saved, cancelled
REVOKEDrspeak (root)rspeak revoke, the first timemessage 12 revoked by admin
REMINDEDrspeak (root)rspeak remindmessage 12: 3 recipients reminded by admin
DELETEDrspeak (root)rspeak purge, at the renamemessage 12 deleted by admin (older than 90d)
POSTPONEDrootspeak-user (U)“Later” or an answer other than yesuser mario: postponed message 12 (terminal)
CONFIRMEDrootspeak-user (U)confirmation recordeduser mario: confirmed reading message 12 (desktop)
CONFIRMATION_FAILEDrootspeak-user (U)confirmation could not be recordeduser mario: ERROR, confirmation of message 12 not recorded (desktop): …
Table 17.2 — The events

17.4 · rspeak log and trustworthiness

rspeak log ID reconstructs the history of a message from the events; the History tab of the TUI shows the same output.

rspeak logrspeak log 12rootjournalsd_journalReaderterminal, TUIloads sent/12/msg: the daterspeak and RSPEAK_MESSAGE=12from the sending date minus 1 sone event at a time, with _UID_UID expected? no: not trustworthy text rewritten from the fields, cleaned
Figure 17.2 — rspeak log: the history of a message, rewritten from the fields
  • Events are searched with two conditions together: SYSLOG_IDENTIFIER=rspeak and RSPEAK_MESSAGE=ID, and only from the sending date of the message (minus one second): a message store that has been reset reuses the numbers, the journal does not.
  • Anyone can write an event with the RSPEAK_* fields to the journal: _UID says who wrote it, not that it is true. rspeak log expects root (in test mode, whoever runs it) for the administrator's events and the recipient themselves for POSTPONED, CONFIRMED and CONFIRMATION_FAILED; if _UID differs, the line begins with “[not trustworthy: written by UID N]” (scenario J3).
  • An event with an unknown name is shown with its MESSAGE; every line goes through clean_text before it reaches the terminal.
A history: two deliveries, the sending and a confirmation from a terminal
admin@server:~$ sudo rspeak log 1
Message 1 · Maintenance · 30/09 20:42
2026-09-30 20:42:25  message 1 delivered to mario (terminals written: 1)
2026-09-30 20:42:25  message 1 delivered to anna (terminals written: 0)
2026-09-30 20:42:25  message 1 sent by admin to: anna mario
2026-09-30 20:43:02  user mario: confirmed reading message 1 (terminal pts/2)
i
Note. writing an event is a separate action from the change it records: if the journal does not respond, the state in the message store stays correct; of the history only the text written with syslog remains, which rspeak log does not read.
Chapter 18

Error taxonomy

18.1 · Three families of errors

RootSpeak distinguishes three families of errors. Those that can be detected before writing stop the command without leaving traces; those that happen during the work do not stop the other recipients and leave a mark; interruptions leave a state that the next command knows how to repair.

How RootSpeak failsinitial checksoptions, text, recipientsfailures during the worka mailbox, a terminalinterruptionssignal, power, SIGKILLdie“rspeak: …”, exit 1, nothing writtenevent + carry onNOT_DELIVERED, then diemarks in the storeincomplete, revoking, .deletingthe administrator correctsand runs the command againthe next command retriesrecovery, warning on stderrthe next command repairsrecover(), idempotent
Figure 18.1 — Three families of errors and who repairs them
Function (src/common.c)What it doesUsed by
die(fmt, …)flushes stdout, writes “rspeak: …” (or “rootspeak-user: …”) to stderr, exits with 1rspeak: every error that stops the command
notice(fmt, …)like die, but the program continuesrecovery, to say what it has completed or cancelled
xmalloc, xrealloc, xasprintfif memory runs out: “out of memory” and exit 1everyone
Table 18.1 — How an error is reported
  • rootspeak-user does not use die in normal work: it runs inside the prompt or in the graphical session, and every problem it meets becomes “do nothing” or a notice to the user (chapter 11.3).
  • In the TUI a die of the command closes only the child process: its message appears in the result dialog (chapter 14.1).

18.2 · The errors of rspeak

MessageWhenWhat remains
unknown command: X (rspeak --help for the guide)first argument not recognisednothing
too many arguments for C: X (rspeak --help for the guide)a command other than send received more arguments than it takes (list none, the others one); X is the first extra onenothing
missing --to · missing value for --title · unknown option: --leveloptions of sendnothing
invalid expiry: X · the expiry is already in the past--expires cannot be interpreted, or is in the pastnothing
FILE: followed by the system's explanation--file not readablenothing
empty message · message too long: N bytes (at most 65536)text after cleaningnothing
no such user: X · no such group: X · no recipients--tonothing
Message N delivered to K of M recipients. · message N not delivered to: … (see the log; the next rspeak command will retry)some copies not writtenincomplete: recovery retries
missing message ID · no such message: Xstatus, revoke, remind, lognothing
message N was cancelled: sending was interrupted before it was savedstatus of a cancelled sending—
message N revoked, but some copies could not be removed from the mailboxes: the next rspeak command will retryrevokerevoked and revoking: recovery retries
message N has been revoked: nobody to remind · message N has expired: nobody to remindremindnothing
missing duration, for example 90d · invalid duration: X (for example 90d, 12h, 30m)purgenothing
/var/lib/rootspeak: … · …/sent/.seq.lock: … · sudo: …the message store cannot be created, the lock cannot be taken, sudo does not start (system messages)nothing, or a skipped number
Table 18.2 — The errors of rspeak
Two errors, and nothing was written
admin@server:~$ sudo rspeak send --to nobodyhere "Test"
rspeak: no such user: nobodyhere
admin@server:~$ sudo rspeak purge 7x
rspeak: invalid duration: 7x (for example 90d, 12h, 30m)

18.3 · Internal results

Inside RootSpeak the functions talk to each other through numeric results. They are the contract between the parts, and the chapters that describe them use them with these values.

FunctionResults
confirm (rootspeak-user)0 recorded or already present · 1 not recorded, the message stays · 2 expired · 3 revoked (chapter 13.1)
ask (rootspeak-user)0 yes · 1 no, or no answer · 2 confirmed elsewhere · 3 revoked · 4 expired while the question was open (chapter 11.3)
zenity0 “I have read it” · 1 “Later” or dialog closed by the user · other: not a choice (chapter 12.3)
as_userthe code of fn · −1 child not started, identity not assumed (126), time limit exceeded, child killed (chapter 7.3)
op_deliver, op_take, op_clear_postponements0 success · 1 failure
read_file, write_atomic0 · −1 with errno (EINVAL if it is not a regular file, EFBIG if too large)
msg_read, msg_from_text0 · −1 if the file is missing, is not regular, is too large or does not have a valid date
summarisetrue · false if the message no longer exists, has no recipients or has been cancelled
Table 18.3 — Internal results
i
Note. code 126 is reserved: a child that cannot assume the identity exits with 126, and as_user treats it as a failure even if fn could have returned it.

18.4 · Recovery warnings and exit status

Warning (stderr)What happenedEvent
completed delivery of message N to K recipientsan interrupted sending has been completedRECOVERED
message N still not delivered to K recipients (see rspeak status N)recovery could not write some copies; it will retry—
message N was interrupted before being saved and has been cancelleda sending without msg or recipients has been closed with failedCANCELLED
Table 18.4 — Recovery warnings

The warnings appear at the start of any command that runs recovery, even one that has nothing to do with that message: rspeak list can therefore say “rspeak: completed delivery of message 2 to 64 recipients” (scenario F3). The command then carries on normally.

CodeMeaning
0completed
1error: the message, which begins with “rspeak:”, says which
othera sudo code, before rspeak starts
Table 18.5 — The exit status of rspeak
Chapter 19

Specification

19.1 · Authoritative sources

This chapter is the contract of RootSpeak: what any implementation must respect, regardless of how it is written. The rest of the manual describes how the C version implements it; the bash prototype (0.1.0) worked on the same message store, and the same tests apply to both. Every change to the code respects this chapter or updates it explicitly.

Sources of truthAuthoritative · rootsent/ID/what, to whom, when, expiry, revocationWorking copy · userusers/U/copy, confirmation, postponementsHistory · journalRSPEAK_* eventswith the _UID of whoever wrote thema recipient's state: sent/ compared with users/U; never from the journal
Figure 19.1 — The three sources and their authority
SourceAuthorityWritten by
sent/ID/ (msg, recipients, delivery, revoked, incomplete, revoking, failed, lock)authoritative: what was sent, to whom, when, with which expiry, to whom it was delivered, whether and when it was revokedroot only
users/U/ (inbox, read, acks, state)working copy, controlled by the user: it can be deleted or altered, and the system must stay consistentthe user (and root, as the user)
journal (SYSLOG_IDENTIFIER=rspeak)history: events with a date and _UID, the user who wrote them (set by the journal). Anyone can write an event with the RSPEAK_* fields: _UID says who wrote it, not that it is true; rspeak log flags as not trustworthy an event written by the wrong user. States are derived from the message store, never from the journalroot and the user
Table 19.1 — The sources of truth
  • A confirmation is a fact declared by the user in their own mailbox; it is valid only if it does not contradict root's message store: its date must not be later than the expiry written in sent/ID/msg nor than the revocation (sent/ID/revoked).
  • “Confirmed” means: the mailbox contains a valid read statement from the user. It is not an action observed by RootSpeak, nor proof of reading: the user controls their own mailbox and could write it themselves. Anyone using rspeak status for an audit must read it this way.
  • If the user deletes their own copy, the message simply shows as not confirmed; if they alter it (text, expiry), only what they see changes.

19.2 · States and transitions

Each recipient of a message is in one of three states; the state is derived from the files, according to fixed rules (chapter 10.1). There are only these three states (the owner's decision): revocation and expiry are properties of the message, not states; incomplete, failed, revoking and .deleting-ID are internal marks with which recovery completes or cancels an interrupted operation, and they are not shown as states. An interrupted sending shows for itself: some recipients stay sent.

The state machineSENTin recipients, not in deliveryDELIVEREDin delivery, no valid confirmationCONFIRMEDacks/ID with a valid datecopy verifiedconfirmation(not expired, not revoked)postponement: state/ID.*Properties of the messageREVOKED · EXPIREDinternal recovery marks, not states:incomplete · failed · revoking · .deleting-ID
Figure 19.2 — The states of a recipient and the properties of a message
TransitionPreconditionAtomic operationEffectEvent
sendingrecipients resolved, text cleaned, at most 65536 bytesID with flock; lock and incomplete before anything else; msg and recipients written, renamed and flushed to disk (fsync)all SENTSENT (at the end of sending)
deliverysending lock held, message not revokedcopy written as the user and renamed, then verified; then a line in delivery; at the end of sending everything to disk, then incomplete removedSENT → DELIVEREDDELIVERED (or NOT_DELIVERED)
confirmationcopy in inbox/, not expired, no confirmation presenttemporary file + link (fails if it exists), then fsyncDELIVERED → CONFIRMEDCONFIRMED
postponementDELIVEREDmodification time of state/ID.term or state/ID.guistays DELIVEREDPOSTPONED
revocationexisting messagesent/ID/revoking and sent/ID/revoked (on disk); then the lock (waits for a sending in progress); then copies removed as the user and revoking removedno state changes; it is no longer shown; later confirmations are not validREVOKED
expiry—none (time passes)it is no longer shown; a confirmation after it is not valid—
recoveryincomplete present and lock freedelivery of the missing copies and check of those already recordedSENT → DELIVEREDDELIVERED, RECOVERED
revocation recoveryrevoking present and lock freeremaining copies removed——
remindermessage neither revoked nor expiredfor each DELIVERED recipient: postponements state/ID.* removed, agent woken upstays DELIVERED; the question and the dialog come back at onceREMINDED
deletionolder than the duration, lock free, completesent/ID renamed to sent/.deleting-ID; then copies, confirmations and postponements removed as the user; then the folder removedthe message no longer existsDELETED
Table 19.2 — The transitions

19.3 · How a message is summarised

In the message list (TUI and rspeak list) a message is summarised by its recipients: how many are in each of the three states, in words and with a bar (confirmed, delivered, sent), plus the properties revoked and expired. No other word describes the state of a message.

The summaryrecipients of sent/IDrecipientsrecipient_stateone of the three, for each14 confirmed · 5 delivered · 2 sent████████▒▒▒░ and [revoked] [expired]no other word describes the state of a message
Figure 19.3 — A message is summarised by the counts of its recipients: “14 confirmed · 5 delivered · 2 sent”, the bar and the marks “[revoked]” and “[expired]”
  • The phrase leaves out zero counts; if everyone has confirmed it says “all confirmed (N)”.
  • Words such as “pending”, “complete”, “incomplete sending”, “cancelled” are not states and do not appear: tests/engine.sh (group 3) and tests/tui.py check this.
  • A sending cancelled before it was saved has no recipients to count: rspeak list reports it with a warning line, the TUI does not show it (chapter 10.4).

19.4 · Concurrent operations

For each command that changes the message store: which lock it holds, at which instant the operation is considered to have happened (the linearisation point: before that instant it has not happened, after it has, for any observer) and what happens with other commands at the same time.

When it happensInstants at which an operation happens for everyonesend: rename recipientsdelivery: delivery linerevoke: rename revokedack: link of acks/IDpurge: rename .deletingbefore that instant the operation has not happened, after it has, for any observer
Figure 19.4 — The linearisation points of the operations that change the message store
OperationLockHappens whenConcurrently
sendsent/.seq.lock for the ID; sent/ID/lock exclusive for the whole deliverythe message: recipients renamed; each delivery: its line in deliveryrecovery and deletion skip the message; revocation stops it at the next recipient
recoverysent/ID/lock if free, otherwise skipslike deliverynever works at the same time as a sending in progress
revokesent/ID/lock waited for after writing revokedsent/ID/revoked renamed into placethe sending in progress delivers nothing more; confirmations with a later date are not valid; the copies are removed once the sending has released the lock
confirmation (rootspeak-user)none: exclusive linkacks/ID created; what counts is the date written insidebetween two confirmations the first one wins; with revocation the date decides: equal or earlier is valid, later is not; without a copy in inbox/ there is no confirmation
purgesent/ID/lock if free, otherwise skipssent/ID renamed to sent/.deleting-IDfrom that instant no command sees the message; list reads what it needs beforehand and skips a message that has disappeared in the meantime
remindnoneeach postponement removedwith a confirmation that arrived in the meantime: that recipient is no longer DELIVERED and removing their postponements has no effect
list, status, log, the TUInone (read-only)—they see the state at that moment: during a sending, the recipients not yet reached are SENT
Table 19.3 — Locks and linearisation points

19.5 · Invariants

#Invariant
I1Every complete sent/ID has msg and recipients; an incomplete one has incomplete, a cancelled one has failed.
I2delivery contains at most one line per recipient, and only for recipients: delivery and recovery hold the same lock.
I3A line in delivery means that the copy was in the mailbox, verified, at that moment.
I4A confirmation comes into being whole or not at all, and once written it does not change: the first one counts.
I5A valid confirmation has a date no later than the expiry of the reference copy nor than the revocation.
I6A message with incomplete and a free lock is an interrupted sending: the first subsequent rspeak command completes it or, if it had not been saved, cancels it.
I7Root never reads or writes the contents of a mailbox with its own identity: always as the user.
I8The saved text and title contain no control characters or escape sequences.
I9IDs always increase (a sequence with flock); they are reused only if the message store is reset.
I10RootSpeak never locks a session and does not touch sessions without a terminal.
I11The wake-up signal goes only to a ready agent; the mailbox remains the authoritative queue: a lost signal delays the dialog by at most RSPEAK_AGENT_POLL seconds, it does not lose it. The signal neither closes nor changes a dialog that is already open.
I12A postponement is recorded only for a choice by the user (“Later”, dialog closed, an answer other than yes): a dialog closed by the system is not a postponement.
I13Durability: when incomplete is gone, copies and delivery lines are on disk; a confirmation is on disk before the user reads “Confirmed.”; revoked is on disk before rspeak revoke touches the mailboxes. After a power failure with incomplete present, recovery rechecks all the copies, including those already recorded.
I14The text is written to a terminal only if the terminal, once opened, belongs to the recipient: the name /dev/pts/N does not identify a session.
I15No root command reads from the mailboxes more than what concerns the messages in sent/, and every read has a time limit: an abnormal mailbox neither blocks nor slows down rspeak.
Table 19.4 — The invariants

19.6 · Interruptions and recovery

For each point at which a process can be interrupted, the state that remains and who repairs it.

InterruptionRemaining stateRepair
after the ID, before the foldera skipped numbernone (harmless)
folder and incomplete, without msg or recipientssending not savedthe next command cancels it (failed, event CANCELLED)
during the deliveriessome recipients SENTthe next command delivers the missing copies
after the deliveries, before removing incompleteeverything delivered, mark left behindthe next command removes the mark
inside the confirmation, before linka temporary file acks/.ID.PID, no confirmationthe question comes back; the temporary file is ignored
after link, before the move to read/confirmation present, copy still in inbox/whoever reads the mailbox completes the move without asking again
leaving the session right after “I have read it”confirmation recorded: the agent waits for the dialog, writes the answer as soon as it arrives and defers SIGTERM until after the writenone (scenario I10: exit as soon as zenity has the answer)
leaving the session with the dialog openno postponement recorded (zenity's exit 1 counts as “Later” only if the display is still there): the dialog comes back at the next loginnone (scenario I13)
the desktop agent dieslock released; one of its dialogs may stay openrspeak send or the login start a new agent; the orphaned dialog no longer records anything
rspeak revoke after revoked, before removing the copiesrevoked and revoking; some copies still in the mailboxesthe next rspeak command removes the copies (until then the recipients do not see them as revoked)
a mailbox unreachable during revocationrevoking remainsthe next command retries
rspeak purge after the renamesent/.deleting-ID, some copies in the mailboxesthe next rspeak command removes copies, confirmations, postponements and the folder
power failure during sendingincomplete on disk; copies and delivery lines perhaps notrecovery rewrites every missing copy, even if recorded
power failure after “Confirmed.”confirmation on disknone
power failure at other momentswrites not flushed to disk may be missing: a postponement (the question comes back earlier) or the move to read/ (completed at the next read)none needed
Table 19.5 — Behaviour after an interruption
i
Note. the repairs are tested: interruptions simulated in tests/engine.sh (groups 1, 2 and 9: copy gone after an interruption, interrupted revocation and deletion), an rspeak send actually killed with SIGKILL (scenario F3) and a revocation during a sending to many users (scenario F7). A real power failure is not tested: durability relies on fsync of files and folders and on syncfs at the end of sending.

19.7 · Time, revocation and expiry

AspectRule
referenceall dates are seconds since the Unix epoch on the machine's clock; users and root use the same clock
expirycompared with the current time when a message is about to be shown, while a question or a dialog is open (every second), and when the confirmation is recorded; in rspeak status with the date of the confirmation
postponementlasts RSPEAK_REMIND_MINUTES from the modification time of state/ID.term or state/ID.gui, each for its own channel; in the terminal, the postponement does not apply at login; for the dialog it applies even after a new graphical login
clock set backexpiries and postponements last longer; no message is lost
clock set forwardexpiries and postponements arrive earlier
suspend and resumethe agent resumes at the next round (at most RSPEAK_AGENT_POLL seconds); the elapsed time counts for expiries and postponements
Table 19.6 — Time
SituationRevocationExpiry
copy not yet shownremoved from the mailbox; not shownstays; no longer shown
dialog or question openthey close (within 1 s)they close (within 1 s)
confirmations already givenremain valid if dated no later than the revocationremain valid if dated no later than the expiry
during sendingsending stops at the next recipientsending continues: whoever receives the copy after the expiry never sees it (“delivered, not confirmed (expired)”)
text already written to the terminalsstays on screenstays on screen
in rspeak status“delivered, not confirmed (revoked)”“delivered, not confirmed (expired)”
Table 19.7 — Revocation and expiry

19.8 · Message store format

Message store format 1. Readers ignore header keys they do not know.

FileContent
sent/.seqthe last ID assigned, a number
sent/ID/msgheader key: value (format, id, from, date, title, expires), blank line, text; UTF-8 without control characters
sent/ID/recipientsone user name per line, no duplicates
sent/ID/deliveryone line per delivery: user date terminals, fields separated by a space; date in seconds since the Unix epoch, terminals the number of terminals written (0 in recovery)
sent/ID/revokedthe revocation date, in seconds since the Unix epoch
sent/ID/failedthe cancellation date; its presence is what counts
sent/ID/incomplete, revoking, lockmarks (the content does not matter): incomplete sending, revocation to be completed; lock is used with flock
sent/.deleting-ID/a message that rspeak purge is deleting
users/U/inbox/ID, read/IDcopy of msg
users/U/acks/IDdate channel: date in seconds since the Unix epoch, a space, channel (desktop or terminal pts/N); the first line counts, at most 200 bytes
users/U/state/ID.term, ID.guiempty; the modification time counts
Table 19.8 — The message store format

19.9 · Supported and verified scope

Requirement
systemLinux with systemd and logind (sessions, systemd-run --user)
libraries and toolsglibc, libsystemd (sessions and journal), sudo, systemd-run and date -d (GNU coreutils); to build: gcc, make, pkg-config and the systemd development files (libsystemd-dev or systemd-devel)
desktopzenity; a session that runs XDG autostart (/etc/xdg/autostart)
question in the terminalbash, zsh or fish at every prompt; sh, dash, ksh and the other shells that read /etc/profile at login; the text at sending time with any shell
Table 19.9 — Requirements

Supported means that the requirements are met and RootSpeak is designed to work; verified that it has been tested, and how. The two are distinct.

EnvironmentSupportedVerified
Debian 13, GNOME on Waylandyesyes: the automated suite in the VM (tests/vm); live on the user's PC with GNOME (sending, dialog, postponement, reminder, revocation, the question in a desktop terminal, sending and the TUI from an ssh session), and the 0.1.0 prototype on GNOME 48
22 distributions with systemd without a desktop: Debian, Ubuntu, Fedora, CentOS Stream, RHEL, Rocky, Alma, Oracle, openSUSE, SLES, Archyesyes: the automated suite in a container on each, built there (tests/distros, chapter 20.4)
zsh, fish, ksh, dashyesyes: K scenarios in the containers, where the distribution packages the shell
other desktops: GNOME on Ubuntu and Rocky (SELinux enforcing), KDE Plasma on Fedora, Cinnamon; Wayland and X11yes, with zenity and XDG autostartyes: the automated suite in a VM for each (tests/vm/desktops)
XFCE, MATE and other desktopsyes, with zenity and XDG autostartno
systems without systemd (sysvinit, OpenRC)no—
Table 19.10 — The scope: supported and verified
Chapter 20

Tests and checks

20.1 · The tests and their results

Every statement about how RootSpeak behaves comes from a run. The tests are organised in levels: at the bottom the internal parts and the engine, fast and without root; at the top the test machine and the VM, where RootSpeak runs as root with real users, sessions and dialogs.

The testsliveafter install.sh, by the usertests/vmVMs with GNOME, KDE Plasma, Cinnamon; root, real dialogstests/container · tests/distrosDebian 13 and 21 other distributions with systemd, root, real ssh sessionstests/engine.sh · tests/tui.py · tests/help-tui.pytest mode, no root, temporary message store, virtual terminalsmake unit · make analyze · make sanitizer · tests/fuzzinternal parts, static analysis, AddressSanitizer and UBSan, libFuzzergoing up: closer to real use, slower; going down: faster, more repeatable
Figure 20.1 — The levels of testing

Test figures are never written by hand: each suite records its result together with the commit and the fingerprint of the code under test (tools/fingerprint.sh: SHA-256 of src/, Makefile, etc/ and install.sh), and this table reads them every time the manual is generated. “Valid for the current code” says whether the recorded fingerprint is that of the code this manual describes.

SuiteResultDateCommitFingerprintValid for the current code
tests/engine.sh76 of 76 checks passed2026-10-01 20:20:37 CEST81199e69f0-modified3368c39af24dyes
tests/container/run.sh69 of 70 scenarios passed, 1 not tested2026-10-01 18:23:39 UTC81199e69f0-modified3368c39af24dyes
tests/vm/run.sh65 of 69 scenarios passed, 4 not tested2026-10-01 18:33:04 UTC81199e69f0-modified3368c39af24dyes
tests/distros/run.sh22 of 22 distributions passed2026-10-01 20:31:37 CEST81199e69f0-modified3368c39af24dyes
Table 20.1 — The last run of each suite (tests/*/results/); fingerprint of the current code: 3368c39af24d
i
Note. the container has no graphical session: the dialog scenario shows up there as “not tested” and is tested in the VM. The VM has no users with zsh, fish, ksh or dash: the shells are tested only in the container. In both, the system language is Italian on purpose: it proves that RootSpeak speaks English whatever the language of the session.

20.2 · The engine: tests/engine.sh

tests/engine.sh tests the engine in test mode, without root, on a fresh temporary message store for each case, with the programs in build/ (or in RSPEAK_BUILD, for example the sanitizer build). Every defect that gets fixed adds checks that fail without the fix.

GroupChecks
1. confirmationsnormal confirmation; write impossible (the message stays, the user is warned, no temporary file, the question comes back and then succeeds); confirmation already present (the first one is kept); confirmation recorded but not moved
2. deliverynormal sending to two recipients; copy not writable for one of them (error, the other one receives it, “sent, not delivered”, incomplete-sending marker); fault fixed and recovery; simulated interruption halfway; recovery that does not bring back a confirmed message; unsaved sending cancelled; sending in progress (lock held) left alone, then completed without duplicates; sending just started not cancelled
3. states--level rejected; delivered after sending; no level in the message; postponement shown as a detail and not asked again at once; confirmed at the next login; rspeak list with the recipients in the three states and no invented state; expired shown as “delivered, not confirmed (expired)”
4. expirythe question stops if the message expires while it is open, no confirmation recorded; a confirmation timed after the expiry of the reference copy is not valid and is not counted
5. configurationthe file is not executed as code; valid values read, invalid, unknown, zero and negative values ignored; the message declares format: 1
6. purgerspeak purge 7d deletes a message from 10 days ago with its copies and confirmations, keeps the recent one and incomplete or in-progress sendings, rejects an invalid duration
7. agentPID written to rootspeak-agent.ready when it is ready; the wake-up signal to that PID does not kill it; the file disappears on exit (only with a graphical session, otherwise skipped)
8. English onlywhatever the language of the session, header, question, rspeak status, errors and help are in English and the administrator's text is untouched; “s” is not a yes (the message is postponed), “Y” confirms; a confirmation from a terminal is shown as “terminal”
9. second reviewrspeak revoke waits for a sending in progress; a confirmation later than the revocation is not valid, one in the same second is valid; copy removed while the user is answering; another user's terminal not written to; copy lost after an interruption rewritten by recovery; interrupted revocation and deletion completed; 5000 fake files in acks/ without slowdowns; a pipe in place of the confirmation; text over 65536 bytes rejected, of 65536 accepted; comments in rootspeak.conf
10. remindera postponed question does not come back at once; rspeak remind brings it back; those who have confirmed are not reminded, and the command says so; a revoked message cannot be reminded
11. argumentsan extra argument is an error and nothing is done (remind 1 1, list extra, revoke 1 2)
Table 20.2 — The groups of tests/engine.sh

At the end tests/engine.sh writes tests/results/engine.json with date, commit, fingerprint and counts; tools/check-docs.py fails if that fingerprint is not the one of the current code. tests/unit-tests.c (make unit) tests the internal parts one by one: text cleaning, messages, configuration, event texts.

20.3 · For real: the test machine and the VM

tests/container/run.sh tests RootSpeak for real in an isolated test machine: a Debian 13 container with systemd running (logind, sshd), without root on the host system. RootSpeak is built there and installed with install.sh; the users are admin (group sudo), mario and anna (group developers), luca, service (without a shell) and four users with other shells: zeno (zsh), fabio (fish), kora (ksh), dario (dash). scenarios.py runs as root, opens real ssh sessions with a terminal, answers the questions as a person would, measures the timings and records every result.

AreaScenarios
A. Accessroot sends; an administrator sends through sudo; an ordinary user cannot run rspeak, not even the help
B. Recipientsuser, list with duplicates, group, all (root and nologin excluded), online, non-existent user, empty group
C. Deliveryuser logged out, then at login; user logged in (time to reach the terminal); ssh without a terminal; two sessions, confirmation in one stopping the other
D. Confirmationpostponement, reminder at the prompt, reminder by the administrator, question again at login, confirmation with its channel
E. Expiry and revocationexpired message not shown; revocation with the question pending; revocation before login
F. Faultscopy not writable and recovery; rspeak send killed with SIGKILL halfway; user folder left owned by root; rspeak revoke during a sending to many users; recovery concurrent with a sending in progress; confirmation not writable
G. Securitymailbox turned into a link to /etc; escape sequences; forged confirmation; RSPEAK_TEST from an ordinary user
J. Logrspeak log rebuilds the history; the confirmation carries the _UID of whoever wrote it; a fake event written by a user is flagged
T. TUIrspeak on its own as root: list with the recipients in the three states, detail and history, exit
K. Shellszsh, fish, ksh and dash: question at login; text on sending; question at the prompt in zsh and fish, not in ksh and dash
L. Languagea user whose session is in Italian: header, question and answer in English, “y” confirms
H. Performancesending to everybody (about sixty users); list with 10 messages; status
I. Desktopnot tested in the container (no graphical session); tested in the VM
Table 20.3 — The real-world scenarios

tests/vm/run.sh runs the same scenarios in a virtual machine with a desktop, on the test server (QEMU with KVM, without root or libvirt), plus the dialog scenarios. Each desktop is a profile in tests/vm/desktops/: the cloud image of its distribution, the packages of the desktop, the automatic login of sara (GDM, SDDM or LightDM) and the accessibility settings. prepare.sh builds the base disk of a profile once with cloud-init (users.sh adds the users), and again when the profile changes; each run starts from a clean copy of it, builds RootSpeak in the VM and puts the system and the desktop in Italian. The dialog buttons are pressed through the desktop's accessibility (desktop.py, AT-SPI), as a person would, and the screenshots come from the QEMU monitor (capture.py).

ProfileSystemDesktopScenarios
debian-gnomeDebian GNU/Linux 13 (trixie)GNOME on Wayland65 of 69 passed
fedora-kdeFedora Linux 44 (Cloud Edition)KDE Plasma on Wayland65 of 69 passed
rocky-gnomeRocky Linux 9.8 (Blue Onyx)GNOME on Wayland65 of 69 passed
ubuntu-cinnamonUbuntu 24.04.5 LTSCinnamon on X1165 of 69 passed
ubuntu-gnomeUbuntu 24.04.5 LTSGNOME (Ubuntu session) on X1165 of 69 passed
Table 20.4 — RootSpeak on each desktop: the last run of tests/vm/run.sh PROFILE (the four shells other than bash are tested in the containers)

Cinnamon is tested on Ubuntu 24.04, the base of Linux Mint 22, because Mint publishes no cloud image; Rocky Linux 9 runs with SELinux enforcing, as RHEL ships it. The measurements of each run are in tests/vm/results/report.md (Debian) and in tests/vm/desktops/results/.

ScenarioWhat is checked and measured
I1 · the dialog appears by itself after sendingseconds to the dialog
I2 · “I have read it” closes the dialog and records the confirmation (desktop)state of the recipient
I12 · desktop session in Italian: the dialog is in Englishtitle and button of the dialog
I3, I4 · “Later”, then the dialog comes back (test postponement: 1 minute)seconds until it comes back
I11 · rspeak remind after “Later”: the dialog comes back at onceseconds until it comes back
I5 · confirmation from a terminal with the dialog openseconds until the dialog closes
I6 · revocation with the dialog openseconds until the dialog closes
I7 · two messages, one dialog after the other, oldest firstorder and confirmations
I8 · agent not running: rspeak send starts itseconds to the dialog
I9 · message while the desktop is closed, then graphical loginseconds after the login restarts
I10 · “I have read it”, then logout as soon as zenity has the answerconfirmation recorded; seconds from the click to the answer
I13 · logout with the dialog openno postponement: the dialog comes back first at the next login
Table 20.5 — The desktop scenarios in the VM
  • RSPEAK_TEST_ONLY=i_desktop runs only the desktop scenarios.
  • Each run writes results.json and report.md to tests/container/results/ and tests/vm/results/, with the measurements of every scenario.

20.4 · On other distributions: tests/distros

tests/distros/run.sh runs the same real-world scenarios on every distribution listed in tests/distros/list: the families of Debian and Ubuntu, Fedora, RHEL and its rebuilds (CentOS Stream, Rocky, Alma, Oracle; RHEL itself through its free UBI images), SUSE (openSUSE and SLES) and Arch. Only distributions with systemd: RootSpeak relies on logind and systemd-run.

tests/distrostests/distros/run.shon the PC: code to the server, results backtest server: 4 test machines at a timeone image per distribution, kept on the SSDin each: setup.sh, then build, install.sh, scenarios.pyapt · dnf · zypper · pacman; the compiler of the distributiontests/distros/results/one folder per distribution, summary.json
Figure 20.2 — The compatibility tests on the distributions

Each distribution gets its own image, built from the official image of the distribution by the same tests/container/Containerfile with tests/container/setup.sh: the package manager tells the family, and the script installs the compiler, sshd, the shells and the test users. The administrators' group is the one of the distribution (sudo or wheel). RootSpeak is then built inside the machine, with the compiler and the libraries of that distribution, and installed with install.sh, as a customer would. The images carry the fingerprint of the preparation: a change to setup.sh builds them again.

A shell that a distribution does not package (ksh on Arch, or the shells missing from the limited repositories of RHEL's UBI images) is “not tested”, not failed; so is a login shell that already stops on the distribution's own files before reaching RootSpeak (dash on Fedora and RHEL: /etc/profile.d/lang.sh is not POSIX).

NameSystemScenariosNot tested, and why
debian-12Debian GNU/Linux 12 (bookworm)69 of 70 passed—
debian-13Debian GNU/Linux 13 (trixie)69 of 70 passed—
ubuntu-22.04Ubuntu 22.04.5 LTS69 of 70 passed—
ubuntu-24.04Ubuntu 24.04.5 LTS69 of 70 passed—
ubuntu-26.04Ubuntu 26.04.1 LTS69 of 70 passed—
fedora-44Fedora Linux 44 (Container Image)65 of 67 passeddash: the login shell already fails on the distribution's own files (-dash: 62: /etc/profile.d/lang.sh: Syntax error: redirection unexpected)
fedora-45Fedora Linux 45 (Container Image Prerelease)69 of 70 passed—
centos-stream-9CentOS Stream 965 of 67 passeddash: the login shell already fails on the distribution's own files (-dash: 62: /etc/profile.d/lang.sh: Syntax error: redirection unexpected)
centos-stream-10CentOS Stream 10 (Coughlan)65 of 67 passeddash: the login shell already fails on the distribution's own files (-dash: 62: /etc/profile.d/lang.sh: Syntax error: redirection unexpected)
rhel-9Red Hat Enterprise Linux 9.8 (Plow)56 of 60 passedfish: not in the distribution's packages; ksh: not in the distribution's packages; dash: not in the distribution's packages
rhel-10Red Hat Enterprise Linux 10.2 (Coughlan)56 of 60 passedfish: not in the distribution's packages; ksh: not in the distribution's packages; dash: not in the distribution's packages
rocky-9Rocky Linux 9.8 (Blue Onyx)65 of 67 passeddash: the login shell already fails on the distribution's own files (-dash: 62: /etc/profile.d/lang.sh: Syntax error: redirection unexpected)
rocky-10Rocky Linux 10.2 (Red Quartz)65 of 67 passeddash: the login shell already fails on the distribution's own files (-dash: 62: /etc/profile.d/lang.sh: Syntax error: redirection unexpected)
alma-9AlmaLinux 9.8 (Olive Jaguar)65 of 67 passeddash: the login shell already fails on the distribution's own files (-dash: 62: /etc/profile.d/lang.sh: Syntax error: redirection unexpected)
alma-10AlmaLinux 10.2 (Lavender Lion)65 of 67 passeddash: the login shell already fails on the distribution's own files (-dash: 62: /etc/profile.d/lang.sh: Syntax error: redirection unexpected)
oracle-9Oracle Linux Server 9.865 of 67 passeddash: the login shell already fails on the distribution's own files (-dash: 62: /etc/profile.d/lang.sh: Syntax error: redirection unexpected)
oracle-10Oracle Linux Server 10.265 of 67 passeddash: the login shell already fails on the distribution's own files (-dash: 62: /etc/profile.d/lang.sh: Syntax error: redirection unexpected)
opensuse-leap-16openSUSE Leap 16.069 of 70 passed—
opensuse-tumbleweedopenSUSE Tumbleweed69 of 70 passed—
sles-15SUSE Linux Enterprise Server 15 SP764 of 66 passedfish: not in the distribution's packages
sles-16SUSE Linux Enterprise Server 16.065 of 67 passeddash: not in the distribution's packages
archArch Linux65 of 67 passedksh: not in the distribution's packages
Table 20.6 — RootSpeak on each distribution: the last run of tests/distros/run.sh (the desktop scenario is never tested in a container)
  • The runs go on the test server (192.168.0.2), four distributions at a time (RSPEAK_PARALLEL); the images live in /media/ROOTSPEAK/distros/storage, on the SSD, because the server's / is in RAM.
  • tests/distros/run.sh fedora-44 arch runs only some of them; the results are in tests/distros/results/ (summary.md, and the report.md of each one).
  • The desktop of other distributions is tested in a VM (tests/vm), not here.

20.5 · The interfaces: tests/tui.py and tests/help-tui.py

The two full-screen interfaces are tested on a pseudo-terminal, pressing keys as a person would and checking the last screen drawn after each one.

TestCheck
openinglist with bar and sentence, correct size; no invented state
Enter, Tabdetail with the recipients and the state from rspeak status; history from the log
n, Ctrl+E, Ctrl+Sform, preview of the recipients, text on two lines; the text goes through the editor and comes back; confirmation with recipients, title and expiry; sending result in a single dialog; the message is in the message store and at the top of the list
m, rreminder confirmed with y and carried out; confirmation before revoking, revocation cancelled with n, not confirmed by s, then carried out with y: “[revoked]” in the list and in the message store
/the search shows only the messages containing the text
80 × 24bar and “confirmed/total” instead of the sentence
qclean exit
60 × 15, pipeno TUI: the help
Table 20.7 — The tests of tests/tui.py
TestCheck
opening at 100 × 30alternate screen, frame, Info tab
→Commands tab
3Options tab
4 Gend of the Examples tab
PgUp gstart of the tab
resize to 70 × 2020 lines of 70 columns
qback to the main screen, exit with code 0
terminal 40 × 8manual-style page through the pager
Table 20.8 — The tests of tests/help-tui.py

tools/tui-screenshots.sh uses the test machine to capture the real TUI screens shown in chapter 14: the colours are the ones the administrator sees.

20.6 · The documentation: tools/check-docs.py

CheckFails if
manuals up to datethe two manuals in docs/ differ from the ones regenerated from the sources
referencea command or option entry of the built-in help is missing from the user manual, has a different description from the source text in src/help.c, or the manual documents one that does not exist
file mapa code file does not appear in the map in chapter 3.1, or the map lists one that does not exist
versionRSPEAK_VERSION does not appear in both manuals
common stylea manual does not embed docs/sources/style.css and docs/sources/manual.js as they are
linksa #anchor link does not lead to any id
teststhe last run of tests/engine.sh does not have the fingerprint of the current code, or has failed checks
Table 20.9 — The checks of tools/check-docs.py

tools/check-docs.py is the last step before recording a change (chapter 3.5): if it passes, it prints “documentation aligned” with the number of help entries, the number of files in the map and the version.

20.7 · Live tests

Before the rewrite in C, the 0.1.0 prototype was tested live on Debian 13 with GNOME 48 on Wayland, with two accounts. On 1 October 2026 the C version, as RootSpeak, was tested live on the user's PC (Debian 13 with GNOME, after ./install.sh): sending from the administrator's session to the account user logged in on the desktop, the dialog, “I have read it” and rspeak status “confirmed … (desktop)”; a second message postponed with “Later”, brought back with rspeak remind and then confirmed. The same day, with the English-only build: the question in a desktop terminal, confirmed with “y” (terminal pts/2); a sending from an administrator connected over ssh; rspeak revoke before any answer; the TUI opened over ssh from a phone. Not yet tested live with the C version: expiry, and the question in a terminal opened over ssh.

Tested (0.1.0 prototype)How
immediate dialog for a logged-in userlive
dialog at login for a user who was not logged inlive, with a second account
text and question in the terminal, also over sshlive, with ssh user@localhost
single confirmation across dialog and terminallive
list, statuslive
revoke with a pending question, expiries, errors, text cleaningin test mode
Table 20.10 — The live tests
!
Warning. live tests as root are run by whoever has the root password, after ./install.sh; in tests run by those working on the code, test mode must always be set (chapter 4.4).

20.8 · The defences of the C code

A root program in C that reads user data must have no memory errors. Besides the behaviour tests, version 0.2.0 has these defences, to be run again whenever the code that reads data changes.

DefenceHow it is runWhat it found
all gcc warnings as errors, and the compiler hardening (chapter 16.3)makenothing to fix: the code was written that way from the start
tests of the internal parts (tests/unit-tests.c)make unit—
AddressSanitizer and UBSan on all the engine and help tests, with reports written to files (none allowed)make sanitizer, then RSPEAK_BUILD=build-san tests/engine.shno memory errors
gcc static analysismake analyzepipes not closed in an error path of as_user (fixed); 3 false alarms remain about the copies of the standard descriptors in a child that exits at once
fuzzing with libFuzzer, in a container with clang, of four targets: text cleaning, message format, output of the mailbox reading, configurationtests/fuzz/run.sh SECONDSa function that assumed an already zeroed structure (fixed); then 30 minutes without defects
comparison with the prototype: text cleaning in C and in bash on 2000 random texts(before removing the prototype)identical results
Table 20.11 — The defences of the C code
i
Note. the results of static analysis, sanitizers and fuzzing have no results file with the fingerprint: they are those of the last run made at the time of the rewrite (30 September 2026).
Chapter 21

Defects found and reviews

21.1 · The two external reviews

The project went through two external critical reviews, carried out by language models on the technical manual, without the code. Every finding was verified on the code or with a test before being accepted; the point-by-point detail is in docs/adversarial-review.md.

History in brief29/9 eveningbash prototype29/9 nightlive tests, 0.1.030/9 morningreviews A and B30/9 afternoonreviews C and D30/9 eveningTUI, reminder1/10RootSpeak, Englishdouble confirmation, slow dialogspecification, three states, suitesrevocation, power loss; 0.2.0 in Cinvented states removedtranslations removed
Figure 21.1 — The milestones of the project and what each one changed
ReviewOn whatWhat came out of it
first (A and B), on version 0.1.0real defects, specification, robustness, testsconfirmations never silently lost; verified delivery with recovery; three states for every message, without levels; structured events and rspeak log; rspeak purge; configuration as data; the “Specification” chapter; the real-world suites in the container and in the VM
second (C and D), on the manual with the covering noteraces, durability, boundariesrevocation serialised with sending; confirmations later than the revocation not valid; terminal checked after opening; durability after a power loss; deletion in a single step; anomalous mailboxes; texts over 65536 bytes rejected; test figures tied to the code fingerprint; “supported” distinguished from “verified”; linearisation points
Table 21.1 — The two reviews
  • Criticisms not accepted, with the reason: “the confirmation does not prove reading” (intended: never block, chapter 19.1); “if the terminal is blocked the message does not arrive” (inaccurate: the mailbox acts as a queue); “rewrite in a compiled language right away” (the specification first; the rewrite came later); rspeak always as root (the user's decision); a pipe in place of acks/ID blocking rspeak status and an end-of-line comment in rootspeak.conf (inaccurate, tested); a /var/lib/rootspeak prepared before installation (outside the model).
  • A criticism rejected by mistake and then accepted: locks left stuck. The kernel releases the lock when the process dies, but the test VM showed that the agent's lock was inherited by its children.

21.2 · The defects found and fixed

DefectFound byFixWhere
an unwritable confirmation was treated as “already confirmed”: the message disappeared without a confirmationreview Apublication with an exclusive link; exit status 1, the message stayschapter 13.1
an interrupted rspeak send left recipients without a copy, never recoveredreview Adelivery verified and recorded; incomplete; recoverychapter 8.6
an agent woken up while starting died (20 out of 20)review A, testhandler for SIGUSR1 as the first instruction; rootspeak-agent.readychapter 12.2
open dialogs and questions accepted the confirmation after the expiryreview Athey close at the expiry; confirmation rejected; date compared with sent/chapter 19.7
recovery, during a long sending, delivered in parallel and recorded duplicatescontainer, F3 and F5sent/ID/lock held for the whole deliverychapter 8.6
online included root and logged-in system accountsVM, B5filter for human userschapter 6.2
the agent's lock was inherited by its children: an orphaned dialog blocked the new agentVMO_CLOEXEC (in the prototype, closing the descriptor)chapter 12.1
logging out of the session right after “I have read it” could lose the confirmationVM, I10signals blocked outside the waits; the dialog is waited for 2 schapter 12.3
the wake-up interrupted the wait for the dialog and recorded a postponement never chosenVM, I7the wake-up does not close the dialogchapter 12.3
a dialog closed by the system counted as a postponementVMpostponement only with zenity exit status 1chapter 12.3
a reminder within one second of “Later” was lostVM, I11the answer is recorded as soon as zenity closes (SIGCHLD)chapter 12.3
rspeak revoke did not wait for a sending in progressreview Crevocation on disk, then the lockchapter 9.1
a confirmation could come into being after the revocationreview Cno confirmation without a copy; date compared with the revocationchapter 13.1
the text could end up on another user's terminalreview Cowner of the opened terminalchapter 8.4
no guarantee after a power lossreview Cfsync, syncfs, recorded copies checked againchapter 19.6
rspeak purge together with rspeak list: a message disappeared halfway through readingreview Drename to .deleting-ID; read before usechapter 9.3
5000 fake files in acks/: rspeak list took 3.9 sreview Donly the messages in sent/, with a time limitchapter 10.2
texts over 128 KiB did not reach the dialogreview Dlimit of 65536 bytes on sendingchapter 8.1
the TUI's sending child never saw the end of the texttests/tui.pyit closes the pipe end it does not usechapter 14.4
pipes not closed in an error path of as_user-fanalyzerclosedchapter 7.3
a function assumed an already zeroed structurefuzzingfixedchapter 20.7
states invented in the TUI and in the specification (“pending”, “complete”, “incomplete sending”, “cancelled”)the user, on the previewsonly the three states; summary with the countschapter 19.3
Table 21.2 — The defects found and fixed

The live tests of the prototype also gave rise to three rules: the first confirmation counts for all channels (one had to confirm twice), waking the agent with a signal (the dialog arrived after 3-4 seconds) and the command in /usr/local/bin instead of sbin (on Debian sbin is not in the PATH of non-root users).

21.3 · From bash to C

Version 0.2.0 is a complete rewrite in C of the bash prototype, decided by the user after the second review: the defects found almost all stemmed from bash (races between check and use, time limits, one process per operation). A language that avoids memory errors by construction had been proposed; the user chose C, with mandatory defences.

In the prototype (bash)In the C version
runuser -u U -- … for every access to a mailboxas_user: a child with the identity of U and a time limit; a mailbox is read in a single child (chapter 7)
set -C, then ln for the confirmationopen(O_EXCL), fsync, link (chapter 13.1)
pkill -F for the wake-upop_wake: ready PID, exact command line, kill as the user (chapter 8.5)
sed and tr for text cleaningclean_text, with the same result on 2000 random texts (chapter 8.7)
configuration read with sourcedata file, two keys (chapter 4.5)
texts in two languages, in shell filesin 0.2.0 gettext catalogues read by RootSpeak; English only from 1 October 2026
wait interrupted by the wake-upppoll with signals blocked outside the waits (chapter 12.2)
Table 21.3 — From bash to C
i
Note. the bash prototype was removed when the C version passed all the tests; it remains in the repository history. The shell hooks remain scripts, and the dialog remains zenity.

21.4 · 1 October 2026: RootSpeak, English only

On 1 October 2026 the product took its final name and became English only.

  • Renamed to RootSpeak: the administrators' command is rspeak, the helper rootspeak-user, the paths /etc/rootspeak and /var/lib/rootspeak; the old name was taken for software in the EU.
  • English only: the translations were removed, because the product is for administrators. Every message, the terminal text, the dialog and the journal text are in English, whatever the language of the session.
  • TUI keys: confirmation dialogs use y (yes) and n (no); the reminder moved to m.
  • Visible names in English: the journal fields (RSPEAK_EVENT, RSPEAK_MESSAGE…), the event names (SENT, CONFIRMED…), the marks in the message store (revoking, .deleting-ID) and the stored channel (terminal pts/N).
Chapter 22

Known limits and open items

22.1 · Known limits

The limits below are known and accepted: they stem from the project's decisions or from what the system offers. Each one says what happens, so that whoever administers the machine knows what to expect.

Shell coveragetext on sendingquestion at loginquestion at promptdialogtested bybashyesyesyesif there is a desktopsuiteszsh, fishyesyesyesif there is a desktopscenarios Kksh, dash, shyesyesnoif there is a desktopscenarios Kother shellsyesif they read profilenoif there is a desktopno
Figure 22.1 — Where a message gets to, shell by shell
LimitConsequence
sh, dash, ksh and similar shells have no hook before the promptthe question arrives at login (and from the desktop); during the session only the text arrives, on sending
zsh installed after RootSpeakyou need to run install.sh again to add the line to the zshrc
--expires dates in words, in Englishthey are interpreted by date -d; the ISO format always works
su does not change the owner of the terminalrspeak send does not write to that terminal on sending; the question still arrives at the prompt
if the desktop agent dies, its dialog stays openthe agent that replaces it opens a second dialog for the same message; the first one no longer records anything
a single agent per user, even with several graphical sessionsthe lock is one per user; the behaviour with several desktops open by the same user has not been tested
rootspeak-agent.ready is written in $XDG_RUNTIME_DIR (or in /tmp if it is missing) and looked for in /run/user/UIDthey coincide in logind sessions; otherwise the wake-up does not find the agent, and this case has not been tested
the postponement on the desktop also holds after a new graphical loginunlike the terminal, where the question always comes back at login (chapter 19.7)
without /dev/tty the question counts as “no”a postponement is recorded; in practice the hook runs rootspeak-user prompt only with a terminal
events written with syslog when the journal is missingrspeak log does not read them
the TUI search is case-insensitive only for unaccented letters“ä” does not find “Ä”
backup and restore of the message store, a real power lossnot tested (chapter 5.6, chapter 19.6)
Table 22.1 — Known limits

22.2 · Open items

ItemStatus
live test of the C version on the user's PCdone on 1 October 2026: sending (also from an ssh session), the dialog, postponement, reminder, the question in a desktop terminal, revocation, the TUI over ssh; still to do: expiry, the question in a terminal opened over ssh
scheduled sending (--at)planned, second version
reusable message templatesplanned, second version
CSV or JSON export for auditplanned, second version
deb and rpm packagesplanned for version 1.0
tests on other distributions and desktopsdone: 22 distributions and 5 desktops (chapter 20.4, chapter 20.3)
rspeak remind: manual reminderdone (chapter 9.2)
tabbed interface for the administratordone: the TUI (chapter 14)
rewrite in a compiled languagedone: version 0.2.0 in C (decision of 30 September)
confirmation question in all shellsdone where the shell allows it (chapter 11.1)
interface in each user's languagedone in 0.2.0, then removed: RootSpeak is for administrators and speaks English only (decision of 1 October)
user reply to the administratornot planned (decision of 30 September)
block level, disconnections, full-screen lockexcluded: the user can always postpone
Table 22.2 — Open and settled items
i
Note. decisions already taken are not reopened: the reasons for them are in docs/decisions-and-history.md.
Chapter 23

Appendix A — Data structures

23.1 · Index of the data structures

This appendix collects the data structures of RootSpeak, with the file that defines each one and the chapter that covers it in depth. They are the “contracts” that run through the code. None is written to disk as it is: only the text formats of chapter 19.8 go into files.

Structuresmsg_tsent/ID/msg, parsedsummary_tmsg_t + three countslist_t (TUI)the summaries, in orderstate_eSENT · DELIVERED · …recipient_tuser, state, linedeliveries_tsent/ID/deliverymailbox_tentries read as Ustate_tstate, date, channeluser_tname, uid, gidop_tmailbox, IDconf_tpostponement, pollingthe state is computed every time: none of these structures is saved
Figure 23.1 — The data structures and how they fit together
StructureDefined inRoleChapter
msg_tsrc/text.ha message that has been read or is to be writtenchapter 5.2
conf_tsrc/text.hthe configurationchapter 4.5
state_esrc/rspeak.hthe three states of a recipientchapter 10.1
summary_tsrc/rspeak.ha message with its recipients counted in the three stateschapter 10.3
recipient_t, recipients_tsrc/rspeak.hthe recipients with the line from rspeak statuschapter 10.4
mailbox_item_t, mailbox_tsrc/store.hconfirmations and postponements read from a mailboxchapter 10.2
user_tsrc/user.ha user: name, UID, primary groupchapter 7.2
as_user_fnsrc/user.hthe function run in the childchapter 7.2
op_tsrc/store.cargument of the mailbox operationschapter 7.4
deliveries_tsrc/rspeak.cthe delivery lines of a messagechapter 10.1
state_tsrc/rspeak.cthe state of a recipient, with date and channel of the confirmationchapter 10.1
buf_t, strv_tsrc/common.hgrowing texts and lists of stringschapter 5.5
screen_t, list_t, form_t, launch_tsrc/tui.cscreen, list, form and command to run in the TUIchapter 14
Table 23.1 — Index of the data structures

23.2 · msg_t: a message

src/text.h
/* Format 1: header "key: value", blank line, text. */
typedef struct {
	long long id;
	char *from;
	long long date;
	char *title;
	long long expires; /* 0: no expiry */
	char *body;
} msg_t;
FieldTypeDescription
idlong longthe message number (id:)
fromchar *the administrator; never null after reading (empty if missing)
datelong longseconds since the Unix epoch; a message without a valid date is not read
titlechar *one line, possibly empty; never null after reading
expireslong longmsg_expired is true if it is not 0 and is less than or equal to the given time
bodychar *everything after the first empty line, with the final line break
Table 23.2 — The fields of msg_t

msg_free frees the three texts and zeroes the structure: a zeroed msg_t can be freed safely.

23.3 · state_e, summary_t, recipient_t

src/rspeak.h
/* The states of a recipient: the only three («Specification» chapter). */
typedef enum { SENT, DELIVERED, CONFIRMED } state_e;

/* A message summarised with its recipients. */
typedef struct {
	long long id;
	msg_t m;
	int confirmed, delivered, sent;
	bool revoked, expired;
} summary_t;

/* The recipients of a message with the status line of rspeak status. */
typedef struct {
	char *user;
	state_e state;
	char *line;
} recipient_t;
state_e
the only place in the code where the states are enumerated. Revoked and expired are not there: they are the two bool fields of summary_t.
summary_t
produced by summarise; confirmed + delivered + sent is the number of recipients. summary_free frees the message it contains.
recipient_t
produced by recipients for rspeak status, for the TUI and for rspeak remind, which reminds those with state == DELIVERED.

23.4 · mailbox_item_t and mailbox_t

src/store.h
typedef struct {
	long long id;
	char *confirm; /* first line of acks/ID, or NULL */
	long long postponement;  /* most recent modification time of state/ID.*, or 0 */
} mailbox_item_t;
typedef struct {
	mailbox_item_t *v;
	size_t n;
} mailbox_t;

A mailbox_t is built by mailbox_from_text, which parses the child's output (A ID line for confirmations, S ID date for postponements), already cleaned; malformed lines are discarded, and for each ID the first confirmation and the most recent postponement count. mailbox_from_text is also exposed for the tests and for fuzzing.

23.5 · user_t, conf_t, buf_t, strv_t

src/user.h, src/text.h, src/common.h (in brief)
typedef struct { char *name; uid_t uid; gid_t gid; } user_t;
typedef int (*as_user_fn)(void *arg);
typedef struct { long remind_minutes; long agent_poll; } conf_t;   /* 30, 60 */
typedef struct { char *s; size_t n, cap; } buf_t;    /* s always terminated by '\0' */
typedef struct { char **v; size_t n, cap; } strv_t;
user_t
from user_find (by name) or user_from_uid; the name is copied.
conf_t
filled in by conf_read with the default values and the valid keys of the file.
buf_t
every text that comes from outside ends up here: buf_add checks for size overflow, and the memory grows by doubling. buf_free brings it back to empty.
strv_t
lists of recipients, of IDs, of terminals; strv_sort in alphabetical order, list_numbers in numerical order.
i
Note. all structures are initialised to zero ({0}) and have a function that frees them; fuzzing found precisely a function that assumed an already zeroed structure (chapter 20.7).
Chapter 24

Glossary

24.1 · Terms A–L

Map of the termsmessagesent/ID/msgrecipientone per linemailboxusers/U/stateone of threechannelterminal, desktopconfirmationacks/IDeventjournalthe glossary terms and how they are connected
Figure 24.1 — The main terms
administrators' group
the existing group sudo, wheel or admin: RootSpeak has no group of its own.
agent
rootspeak-user agent: shows the confirmation dialogs in the graphical session; one per user (chapter 12).
as_user
the function in src/user.c that runs an operation in a child process with a user's identity, with a time limit (chapter 7).
author
the administrator who ran rspeak (SUDO_USER): the sender of the messages and the author of the events.
channel
where a confirmation comes from: desktop or terminal pts/N; it is stored as it is shown.
confirmation
the user's statement of having read the message, in acks/ID, with date and channel; the first one counts (chapter 13).
confirmed
state of a recipient with a valid confirmation: not later than the expiry or the revocation. It is a statement by the user, not a proof of reading.
delivered
state of a recipient whose copy has been written and verified in the mailbox (line in sent/ID/delivery) and who has no valid confirmation.
event
a structured line in the journal, with the RSPEAK_* fields (chapter 17).
expired
property of a message whose expiry has passed: it is no longer shown, and later confirmations are not valid.
fingerprint
the abbreviated SHA-256 of the code (tools/fingerprint.sh), recorded with the test results (chapter 20.1).
hook
the file that connects RootSpeak to the shell: rootspeak.bash, rootspeak.zsh, rootspeak.fish and, for the other shells, /etc/profile.d/rootspeak.sh (chapter 11).
internal marker
incomplete, revoking, failed, .deleting-ID: files through which recovery knows what to complete or cancel. They are not states.
linearisation point
the instant at which an operation is considered to have happened for any observer (chapter 19.4).
lock
the file sent/ID/lock used with flock: held by rspeak send for the whole delivery, it tells a sending in progress from an interrupted one.

24.2 · Terms M–Z

mailbox
a user's folder /var/lib/rootspeak/users/U, owned by the user, with permissions 700.
message
a text with an optional title and expiry and a sequential number (ID).
message properties
revoked and expired: they apply to the message, not to a recipient, and they are not states.
message store
the folder /var/lib/rootspeak: root's sent/ and the users' mailboxes (chapter 5).
postponement
the answer “no” or “Later”: the file state/ID.term or state/ID.gui, whose modification time counts for RSPEAK_REMIND_MINUTES minutes.
ready
an agent that has set up the reception of the wake-up signal and has written its PID to rootspeak-agent.ready.
recipient
a user the message is addressed to; the list is in sent/ID/recipients.
recovery
recover: completes interrupted sendings, revocations and deletions at the start of the following commands (chapter 8.6).
reference copy
the message in sent/ID/msg, accessible only to root: the only expiry that counts.
reminder
rspeak remind: removes the postponements of the delivered recipients and wakes up their agent (chapter 9.2).
revoked
property of a message with sent/ID/revoked: it is no longer shown, and later confirmations are not valid.
sent
state of a recipient who does not yet have the copy in the mailbox.
summary
a message described by the counts of its recipients in the three states, in words and with a bar (chapter 10.3).
test mode
RSPEAK_TEST=1 with a test message store: the code is tested without root (chapter 4.3).
TUI
the full-screen interface for administrators: rspeak without arguments in a terminal (chapter 14).
wake-up
the SIGUSR1 signal that rspeak send and rspeak remind send to the ready agent.
zenity
the program that draws the “I have read it” / “Later” dialog on the desktop.
Chapter 25

Index

The terms of the glossary, in alphabetical order, with their definition and the chapters that the definition refers to.